/* * Copyright © 2014 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 . * * Authors: * Alexander Larsson */ #include "config.h" #include "flatpak-utils.h" #include "lib/flatpak-error.h" #include "flatpak-dir.h" #include "flatpak-portal-error.h" #include "flatpak-oci-registry.h" #include "flatpak-run.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "libglnx/libglnx.h" #include #include /* 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"}, }; GLNX_DEFINE_CLEANUP_FUNCTION (void *, flatpak_local_free_read_archive, archive_read_free) #define free_read_archive __attribute__((cleanup (flatpak_local_free_read_archive))) static void propagate_libarchive_error (GError **error, struct archive *a) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "%s", archive_error_string (a)); } GQuark flatpak_error_quark (void) { static volatile gsize quark_volatile = 0; g_dbus_error_register_error_domain ("flatpak-error-quark", &quark_volatile, flatpak_error_entries, G_N_ELEMENTS (flatpak_error_entries)); return (GQuark) quark_volatile; } GFile * flatpak_file_new_tmp_in (GFile *dir, const char *template, GError **error) { glnx_fd_close int tmp_fd = -1; g_autofree char *tmpl = g_build_filename (flatpak_file_get_path_cached (dir), template, NULL); tmp_fd = g_mkstemp_full (tmpl, O_RDWR, 0644); if (tmp_fd == -1) { glnx_set_error_from_errno (error); return NULL; } return g_file_new_for_path (tmpl); } gboolean flatpak_write_update_checksum (GOutputStream *out, gconstpointer data, gsize len, gsize *out_bytes_written, GChecksum *checksum, GCancellable *cancellable, GError **error) { if (out) { if (!g_output_stream_write_all (out, data, len, out_bytes_written, cancellable, error)) return FALSE; } else if (out_bytes_written) { *out_bytes_written = len; } if (checksum) g_checksum_update (checksum, data, len); return TRUE; } gboolean flatpak_splice_update_checksum (GOutputStream *out, GInputStream *in, GChecksum *checksum, GCancellable *cancellable, GError **error) { gsize bytes_read, bytes_written; char buf[32*1024]; do { if (!g_input_stream_read_all (in, buf, sizeof buf, &bytes_read, cancellable, error)) return FALSE; if (!flatpak_write_update_checksum (out, buf, bytes_read, &bytes_written, checksum, cancellable, error)) return FALSE; } while (bytes_read > 0); return TRUE; } GBytes * flatpak_read_stream (GInputStream *in, gboolean null_terminate, GError **error) { g_autoptr(GOutputStream) mem_stream = NULL; mem_stream = g_memory_output_stream_new_resizable (); if (g_output_stream_splice (mem_stream, in, 0, NULL, error) < 0) return NULL; if (null_terminate) { if (!g_output_stream_write (G_OUTPUT_STREAM (mem_stream), "\0", 1, NULL, error)) return NULL; } if (!g_output_stream_close (G_OUTPUT_STREAM (mem_stream), NULL, error)) return NULL; return g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (mem_stream)); } gint flatpak_strcmp0_ptr (gconstpointer a, gconstpointer b) { return g_strcmp0 (*(char * const *) a, *(char * const *) b); } /* Compares if str has a specific path prefix. This differs from a regular prefix in two ways. First of all there may be multiple slashes separating the path elements, and secondly, if a prefix is matched that has to be en entire path element. For instance /a/prefix matches /a/prefix/foo/bar, but not /a/prefixfoo/bar. */ gboolean flatpak_has_path_prefix (const char *str, const char *prefix) { while (TRUE) { /* Skip consecutive slashes to reach next path element */ while (*str == '/') str++; while (*prefix == '/') prefix++; /* No more prefix path elements? Done! */ if (*prefix == 0) return TRUE; /* Compare path element */ while (*prefix != 0 && *prefix != '/') { if (*str != *prefix) return FALSE; str++; prefix++; } /* Matched prefix path element, must be entire str path element */ if (*str != '/' && *str != 0) return FALSE; } } /* Returns end of matching path prefix, or NULL if no match */ const char * flatpak_path_match_prefix (const char *pattern, const char *string) { char c, test; const char *tmp; while (*pattern == '/') pattern++; while (*string == '/') string++; while (TRUE) { switch (c = *pattern++) { case 0: if (*string == '/' || *string == 0) return string; return NULL; case '?': if (*string == '/' || *string == 0) return NULL; string++; break; case '*': c = *pattern; while (c == '*') c = *++pattern; /* special case * at end */ if (c == 0) { 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; } } return NULL; /* Should not be reached */ } static const char * flatpak_get_kernel_arch (void) { static struct utsname buf; static char *arch = NULL; char *m; if (arch != NULL) return arch; if (uname (&buf)) { arch = "unknown"; return arch; } /* By default, just pass on machine, good enough for most arches */ arch = buf.machine; /* Override for some arches */ m = buf.machine; /* i?86 */ if (strlen (m) == 4 && m[0] == 'i' && m[2] == '8' && m[3] == '6') { arch = "i386"; } else if (g_str_has_prefix (m, "arm")) { if (g_str_has_suffix (m, "b")) arch = "armeb"; else arch = "arm"; } else if (strcmp (m, "mips") == 0) { #if G_BYTE_ORDER == G_LITTLE_ENDIAN arch = "mipsel"; #endif } else if (strcmp (m, "mips64") == 0) { #if G_BYTE_ORDER == G_LITTLE_ENDIAN arch = "mips64el"; #endif } return arch; } /* This maps the kernel-reported uname to a single string representing * the cpu family, in the sense that all members of this family would * be able to understand and link to a binary file with such cpu * opcodes. That doesn't necessarily mean that all members of the * family can run all opcodes, for instance for modern 32bit intel we * report "i386", even though they support instructions that the * original i386 cpu cannot run. Still, such an executable would * at least try to execute a 386, whereas an arm binary would not. */ const char * flatpak_get_arch (void) { /* Avoid using uname on multiarch machines, because uname reports the kernels * arch, and that may be different from userspace. If e.g. the kernel is 64bit and * the userspace is 32bit we want to use 32bit by default. So, we take the current build * arch as the default. */ #if defined(__i386__) return "i386"; #elif defined(__x86_64__) return "x86_64"; #elif defined(__aarch64__) return "aarch64"; #elif defined(__arm__) #if G_BYTE_ORDER == G_LITTLE_ENDIAN return "arm"; #else return "armeb"; #endif #else return flatpak_get_kernel_arch (); #endif } /* Get all compatible arches for this host in order of priority */ const char ** flatpak_get_arches (void) { static gsize arches = 0; static struct { const char *kernel_arch; const char *compat_arch; } compat_arches[] = { { "x86_64", "i386" }, { "aarch64", "arm" }, }; if (g_once_init_enter (&arches)) { gsize new_arches = 0; const char *main_arch = flatpak_get_arch (); const char *kernel_arch = flatpak_get_kernel_arch (); GPtrArray *array = g_ptr_array_new (); int i; /* This is the userspace arch, i.e. the one flatpak itself was build for. It's always first. */ g_ptr_array_add (array, (char *)main_arch); /* Also add all other arches that are compatible with the kernel arch */ for (i = 0; i < G_N_ELEMENTS(compat_arches); i++) { if ((strcmp (compat_arches[i].kernel_arch, kernel_arch) == 0) && /* Don't re-add the main arch */ (strcmp (compat_arches[i].compat_arch, main_arch) != 0)) g_ptr_array_add (array, (char *)compat_arches[i].compat_arch); } g_ptr_array_add (array, NULL); new_arches = (gsize)g_ptr_array_free (array, FALSE); g_once_init_leave (&arches, new_arches); } return (const char **)arches; } const char ** flatpak_get_gl_drivers (void) { static gsize drivers = 0; if (g_once_init_enter (&drivers)) { gsize new_drivers; char **new_drivers_c = 0; const char *env = g_getenv ("FLATPAK_GL_DRIVERS"); if (env != NULL && *env != 0) new_drivers_c = g_strsplit (env, ":", -1); else { g_autofree char *nvidia_version = NULL; char *dot; GPtrArray *array = g_ptr_array_new (); if (g_file_get_contents ("/sys/module/nvidia/version", &nvidia_version, NULL, NULL)) { g_strstrip (nvidia_version); /* Convert dots to dashes */ while ((dot = strchr (nvidia_version, '.')) != NULL) *dot = '-'; g_ptr_array_add (array, g_strconcat ("nvidia-", nvidia_version, NULL)); } g_ptr_array_add (array, (char *)"default"); g_ptr_array_add (array, (char *)"host"); g_ptr_array_add (array, NULL); new_drivers_c = (char **)g_ptr_array_free (array, FALSE); } new_drivers = (gsize)new_drivers_c; g_once_init_leave (&drivers, new_drivers); } return (const char **)drivers; } gboolean flatpak_is_in_sandbox (void) { static gsize in_sandbox = 0; if (g_once_init_enter (&in_sandbox)) { g_autofree char *path = g_build_filename (g_get_user_runtime_dir (), "flatpak-info", NULL); gsize new_in_sandbox; new_in_sandbox = 2; if (g_file_test (path, G_FILE_TEST_IS_REGULAR)) new_in_sandbox = 1; g_once_init_leave (&in_sandbox, new_in_sandbox); } return in_sandbox == 1; } const char * flatpak_get_bwrap (void) { const char *e = g_getenv ("FLATPAK_BWRAP"); if (e != NULL) return e; return HELPER; } gboolean flatpak_break_hardlink (GFile *file, GError **error) { g_autofree char *path = g_file_get_path (file); struct stat st_buf; if (stat (path, &st_buf) == 0 && st_buf.st_nlink > 1) { g_autoptr(GFile) dir = g_file_get_parent (file); g_autoptr(GFile) tmp = NULL; tmp = flatpak_file_new_tmp_in (dir, ".breaklinkXXXXXX", error); if (tmp == NULL) return FALSE; if (!g_file_copy (file, tmp, G_FILE_COPY_OVERWRITE, NULL, NULL, NULL, error)) return FALSE; if (rename (flatpak_file_get_path_cached (tmp), path) != 0) { glnx_set_error_from_errno (error); return FALSE; } } return TRUE; } /* We only migrate the user dir, because thats what most people used with xdg-app, * and its where all per-user state/config are stored. */ void flatpak_migrate_from_xdg_app (void) { g_autofree char *source = g_build_filename (g_get_user_data_dir (), "xdg-app", NULL); g_autofree char *dest = g_build_filename (g_get_user_data_dir (), "flatpak", NULL); if (!g_file_test (dest, G_FILE_TEST_EXISTS) && g_file_test (source, G_FILE_TEST_EXISTS)) { g_print ("Migrating %s to %s\n", source, dest); if (rename (source, dest) != 0) { if (errno != ENOENT && errno != ENOTEMPTY && errno != EEXIST) g_print ("Error during migration: %s\n", g_strerror (errno)); } } } 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_is_valid_name (const char *string, GError **error) { 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)) { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Name can't be empty"); goto out; } if (G_UNLIKELY (len > 255)) { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Name can't be longer than 255 characters"); goto out; } end = string + len; last_dot = strrchr (string, '.'); last_element = FALSE; s = string; if (G_UNLIKELY (*s == '.')) { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Name can't start with a period"); goto out; } else if (G_UNLIKELY (!is_valid_initial_name_character (*s, last_element))) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "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)) { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Name can't end with a period"); goto out; } if (!is_valid_initial_name_character (*s, last_element)) { if (*s == '-') g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Only last name segment can contain -"); else g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "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 == '-') g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Only last name segment can contain -"); else g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Name can't contain %c", *s); goto out; } s += 1; } if (G_UNLIKELY (dot_count < 2)) { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, "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); } 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 digit. * 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)) { g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Branch can't be empty"); goto out; } end = string + len; s = string; if (G_UNLIKELY (!is_valid_initial_branch_character (*s))) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Branch can't start with %c", *s); goto out; } s += 1; while (s != end) { if (G_UNLIKELY (!is_valid_branch_character (*s))) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Branch can't contain %c", *s); goto out; } s += 1; } ret = TRUE; out: return ret; } 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, "Wrong number of components in %s", full_ref); return NULL; } if (strcmp (parts[0], "app") != 0 && strcmp (parts[0], "runtime") != 0) { flatpak_fail (error, "Not application or runtime"); return NULL; } if (!flatpak_is_valid_name (parts[1], &local_error)) { flatpak_fail (error, "Invalid name %s: %s", parts[1], local_error->message); return NULL; } if (strlen (parts[2]) == 0) { flatpak_fail (error, "Invalid arch %s", parts[2]); return NULL; } if (!flatpak_is_valid_branch (parts[3], &local_error)) { flatpak_fail (error, "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, "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, "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, "'%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, "'%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 *branch, const char *arch, 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, branch, arch, 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, branch, arch, 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 *branch, const char *arch, 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(GError) my_error = NULL; const char *key; GHashTableIter iter; 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, branch, arch, hash, cancellable, &my_error)) { g_autoptr(GPtrArray) system_dirs = NULL; int i; system_dirs = flatpak_dir_get_system_list (cancellable, error); if (system_dirs == NULL) goto out; for (i = 0; i < system_dirs->len; i++) { FlatpakDir *system_dir = g_ptr_array_index (system_dirs, i); g_clear_error (&my_error); if (flatpak_dir_collect_unmaintained_refs (system_dir, name_prefix, branch, arch, hash, cancellable, &my_error)) { /* Reference found in at least one of the system installations */ break; } } } 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; if (ret == NULL) g_propagate_error (error, g_steal_pointer (&my_error)); out: 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) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, _("%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); } FlatpakDeploy * flatpak_find_deploy_for_ref (const char *ref, GCancellable *cancellable, GError **error) { g_autoptr(FlatpakDir) user_dir = NULL; g_autoptr(GPtrArray) system_dirs = NULL; g_autoptr(FlatpakDeploy) deploy = NULL; g_autoptr(GError) my_error = NULL; user_dir = flatpak_dir_get_user (); system_dirs = flatpak_dir_get_system_list (cancellable, error); if (system_dirs == NULL) return NULL; deploy = flatpak_dir_load_deployed (user_dir, ref, NULL, cancellable, &my_error); if (deploy == NULL && system_dirs->len > 0 && g_error_matches (my_error, FLATPAK_ERROR, FLATPAK_ERROR_NOT_INSTALLED)) { int i; for (i = 0; deploy == NULL && i < system_dirs->len; i++) { FlatpakDir *system_dir = g_ptr_array_index (system_dirs, i); g_clear_error (&my_error); deploy = flatpak_dir_load_deployed (system_dir, ref, NULL, cancellable, &my_error); } } if (deploy == NULL) g_propagate_error (error, g_steal_pointer (&my_error)); return g_steal_pointer (&deploy); } static gboolean overlay_symlink_tree_dir (int source_parent_fd, const char *source_name, const char *source_symlink_prefix, int destination_parent_fd, const char *destination_name, GCancellable *cancellable, GError **error) { gboolean ret = FALSE; int res; g_auto(GLnxDirFdIterator) source_iter = { 0 }; glnx_fd_close int destination_dfd = -1; struct dirent *dent; if (!glnx_dirfd_iterator_init_at (source_parent_fd, source_name, FALSE, &source_iter, error)) goto out; do res = mkdirat (destination_parent_fd, destination_name, 0777); while (G_UNLIKELY (res == -1 && errno == EINTR)); if (res == -1) { if (errno != EEXIST) { glnx_set_error_from_errno (error); goto out; } } if (!glnx_opendirat (destination_parent_fd, destination_name, TRUE, &destination_dfd, error)) goto out; while (TRUE) { if (!glnx_dirfd_iterator_next_dent_ensure_dtype (&source_iter, &dent, cancellable, error)) goto out; if (dent == NULL) break; if (dent->d_type == DT_DIR) { g_autofree gchar *target = g_build_filename ("..", source_symlink_prefix, dent->d_name, NULL); if (!overlay_symlink_tree_dir (source_iter.fd, dent->d_name, target, destination_dfd, dent->d_name, cancellable, error)) goto out; } else { g_autofree gchar *target = g_build_filename (source_symlink_prefix, dent->d_name, NULL); if (unlinkat (destination_dfd, dent->d_name, 0) != 0 && errno != ENOENT) { glnx_set_error_from_errno (error); goto out; } if (symlinkat (target, destination_dfd, dent->d_name) != 0) { glnx_set_error_from_errno (error); goto out; } } } ret = TRUE; out: return ret; } gboolean flatpak_overlay_symlink_tree (GFile *source, GFile *destination, const char *symlink_prefix, GCancellable *cancellable, GError **error) { gboolean ret = FALSE; if (!flatpak_mkdir_p (destination, cancellable, error)) goto out; /* The fds are closed by this call */ if (!overlay_symlink_tree_dir (AT_FDCWD, flatpak_file_get_path_cached (source), symlink_prefix, AT_FDCWD, flatpak_file_get_path_cached (destination), cancellable, error)) goto out; ret = TRUE; out: return ret; } 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; } /* Based on g_mkstemp from glib */ gint flatpak_mkstempat (int dir_fd, gchar *tmpl, int flags, int mode) { char *XXXXXX; int count, fd; static const char letters[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; static const int NLETTERS = sizeof (letters) - 1; glong value; GTimeVal tv; static int counter = 0; g_return_val_if_fail (tmpl != NULL, -1); /* find the last occurrence of "XXXXXX" */ XXXXXX = g_strrstr (tmpl, "XXXXXX"); if (!XXXXXX || strncmp (XXXXXX, "XXXXXX", 6)) { errno = EINVAL; return -1; } /* Get some more or less random data. */ g_get_current_time (&tv); value = (tv.tv_usec ^ tv.tv_sec) + counter++; for (count = 0; count < 100; value += 7777, ++count) { glong v = value; /* Fill in the random bits. */ XXXXXX[0] = letters[v % NLETTERS]; v /= NLETTERS; XXXXXX[1] = letters[v % NLETTERS]; v /= NLETTERS; XXXXXX[2] = letters[v % NLETTERS]; v /= NLETTERS; XXXXXX[3] = letters[v % NLETTERS]; v /= NLETTERS; XXXXXX[4] = letters[v % NLETTERS]; v /= NLETTERS; XXXXXX[5] = letters[v % NLETTERS]; fd = openat (dir_fd, tmpl, flags | O_CREAT | O_EXCL, mode); if (fd >= 0) return fd; else if (errno != EEXIST) /* Any other error will apply also to other names we might * try, and there are 2^32 or so of them, so give up now. */ return -1; } /* We got out of the loop because we ran out of combinations to try. */ errno = EEXIST; return -1; } struct FlatpakTablePrinter { GPtrArray *rows; GPtrArray *current; int n_columns; }; FlatpakTablePrinter * flatpak_table_printer_new (void) { FlatpakTablePrinter *printer = g_new0 (FlatpakTablePrinter, 1); printer->rows = g_ptr_array_new_with_free_func ((GDestroyNotify) g_strfreev); printer->current = g_ptr_array_new_with_free_func (g_free); return printer; } void flatpak_table_printer_free (FlatpakTablePrinter *printer) { g_ptr_array_free (printer->rows, TRUE); g_ptr_array_free (printer->current, TRUE); g_free (printer); } void flatpak_table_printer_add_column (FlatpakTablePrinter *printer, const char *text) { g_ptr_array_add (printer->current, text ? g_strdup (text) : g_strdup ("")); } void flatpak_table_printer_add_column_len (FlatpakTablePrinter *printer, const char *text, gsize len) { g_ptr_array_add (printer->current, text ? g_strndup (text, len) : g_strdup ("")); } void flatpak_table_printer_append_with_comma (FlatpakTablePrinter *printer, const char *text) { char *old, *new; g_assert (printer->current->len > 0); old = g_ptr_array_index (printer->current, printer->current->len - 1); if (old[0] != 0) new = g_strconcat (old, ",", text, NULL); else new = g_strdup (text); g_ptr_array_index (printer->current, printer->current->len - 1) = new; g_free (old); } void flatpak_table_printer_finish_row (FlatpakTablePrinter *printer) { if (printer->current->len == 0) return; /* Ignore empty rows */ printer->n_columns = MAX (printer->n_columns, printer->current->len); g_ptr_array_add (printer->current, NULL); g_ptr_array_add (printer->rows, g_ptr_array_free (printer->current, FALSE)); printer->current = g_ptr_array_new_with_free_func (g_free); } void flatpak_table_printer_print (FlatpakTablePrinter *printer) { g_autofree int *widths = NULL; int i, j; if (printer->current->len != 0) flatpak_table_printer_finish_row (printer); widths = g_new0 (int, printer->n_columns); for (i = 0; i < printer->rows->len; i++) { char **row = g_ptr_array_index (printer->rows, i); for (j = 0; row[j] != NULL; j++) widths[j] = MAX (widths[j], strlen (row[j])); } for (i = 0; i < printer->rows->len; i++) { char **row = g_ptr_array_index (printer->rows, i); for (j = 0; row[j] != NULL; j++) g_print ("%s%-*s", (j == 0) ? "" : " ", widths[j], row[j]); g_print ("\n"); } } static GHashTable *app_ids; typedef struct { char *name; GKeyFile *app_info; gboolean exited; GList *pending; } AppInfo; static void app_info_free (AppInfo *info) { g_free (info->name); g_key_file_unref (info->app_info); g_free (info); } static void ensure_app_ids (void) { if (app_ids == NULL) app_ids = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify) app_info_free); } /* Returns NULL on failure, keyfile with name "" if not sandboxed, and full app-info otherwise */ static GKeyFile * parse_app_id_from_fileinfo (int pid) { g_autofree char *root_path = NULL; glnx_fd_close int root_fd = -1; glnx_fd_close int info_fd = -1; struct stat stat_buf; g_autoptr(GError) local_error = NULL; g_autoptr(GMappedFile) mapped = NULL; g_autoptr(GKeyFile) metadata = NULL; root_path = g_strdup_printf ("/proc/%u/root", pid); root_fd = openat (AT_FDCWD, root_path, O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY); if (root_fd == -1) { /* Not able to open the root dir shouldn't happen. Probably the app died and we're failing due to /proc/$pid not existing. In that case fail instead of treating this as privileged. */ g_debug ("Unable to open %s", root_path); return NULL; } metadata = g_key_file_new (); info_fd = openat (root_fd, ".flatpak-info", O_RDONLY | O_CLOEXEC | O_NOCTTY); if (info_fd == -1) { if (errno == ENOENT) { /* No file => on the host */ g_key_file_set_string (metadata, "Application", "name", ""); return g_steal_pointer (&metadata); } return NULL; /* Some weird error => failure */ } if (fstat (info_fd, &stat_buf) != 0 || !S_ISREG (stat_buf.st_mode)) return NULL; /* Some weird fd => failure */ mapped = g_mapped_file_new_from_fd (info_fd, FALSE, &local_error); if (mapped == NULL) { g_warning ("Can't map .flatpak-info file: %s", local_error->message); return NULL; } if (!g_key_file_load_from_data (metadata, g_mapped_file_get_contents (mapped), g_mapped_file_get_length (mapped), G_KEY_FILE_NONE, &local_error)) { g_warning ("Can't load .flatpak-info file: %s", local_error->message); return NULL; } return g_steal_pointer (&metadata); } static void got_credentials_cb (GObject *source_object, GAsyncResult *res, gpointer user_data) { AppInfo *info = user_data; g_autoptr(GDBusMessage) reply = NULL; g_autoptr(GError) error = NULL; GList *l; reply = g_dbus_connection_send_message_with_reply_finish (G_DBUS_CONNECTION (source_object), res, &error); if (!info->exited && reply != NULL) { GVariant *body = g_dbus_message_get_body (reply); guint32 pid; g_variant_get (body, "(u)", &pid); info->app_info = parse_app_id_from_fileinfo (pid); } for (l = info->pending; l != NULL; l = l->next) { GTask *task = l->data; if (info->app_info == NULL) g_task_return_new_error (task, FLATPAK_PORTAL_ERROR, FLATPAK_PORTAL_ERROR_FAILED, "Can't find app id"); else g_task_return_pointer (task, g_key_file_ref (info->app_info), (GDestroyNotify)g_key_file_unref); } g_list_free_full (info->pending, g_object_unref); info->pending = NULL; if (info->app_info == NULL) g_hash_table_remove (app_ids, info->name); } void flatpak_invocation_lookup_app_info (GDBusMethodInvocation *invocation, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { GDBusConnection *connection = g_dbus_method_invocation_get_connection (invocation); const gchar *sender = g_dbus_method_invocation_get_sender (invocation); g_autoptr(GTask) task = NULL; AppInfo *info; task = g_task_new (invocation, cancellable, callback, user_data); ensure_app_ids (); info = g_hash_table_lookup (app_ids, sender); if (info == NULL) { info = g_new0 (AppInfo, 1); info->name = g_strdup (sender); g_hash_table_insert (app_ids, info->name, info); } if (info->app_info) { g_task_return_pointer (task, g_key_file_ref (info->app_info), (GDestroyNotify)g_key_file_unref); } else { if (info->pending == NULL) { g_autoptr(GDBusMessage) msg = g_dbus_message_new_method_call ("org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "GetConnectionUnixProcessID"); g_dbus_message_set_body (msg, g_variant_new ("(s)", sender)); g_dbus_connection_send_message_with_reply (connection, msg, G_DBUS_SEND_MESSAGE_FLAGS_NONE, 30000, NULL, cancellable, got_credentials_cb, info); } info->pending = g_list_prepend (info->pending, g_object_ref (task)); } } GKeyFile * flatpak_invocation_lookup_app_info_finish (GDBusMethodInvocation *invocation, GAsyncResult *result, GError **error) { return g_task_propagate_pointer (G_TASK (result), error); } static void name_owner_changed (GDBusConnection *connection, const gchar *sender_name, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { const char *name, *from, *to; g_variant_get (parameters, "(sss)", &name, &from, &to); ensure_app_ids (); if (name[0] == ':' && strcmp (name, from) == 0 && strcmp (to, "") == 0) { AppInfo *info = g_hash_table_lookup (app_ids, name); if (info != NULL) { info->exited = TRUE; if (info->pending == NULL) g_hash_table_remove (app_ids, name); } } } void flatpak_connection_track_name_owners (GDBusConnection *connection) { g_dbus_connection_signal_subscribe (connection, "org.freedesktop.DBus", "org.freedesktop.DBus", "NameOwnerChanged", "/org/freedesktop/DBus", NULL, G_DBUS_SIGNAL_FLAGS_NONE, name_owner_changed, NULL, NULL); } typedef struct { GError *error; GError *splice_error; GMainLoop *loop; int refs; } SpawnData; static void spawn_data_exit (SpawnData *data) { data->refs--; if (data->refs == 0) g_main_loop_quit (data->loop); } static void spawn_output_spliced_cb (GObject *obj, GAsyncResult *result, gpointer user_data) { SpawnData *data = user_data; g_output_stream_splice_finish (G_OUTPUT_STREAM (obj), result, &data->splice_error); spawn_data_exit (data); } static void spawn_exit_cb (GObject *obj, GAsyncResult *result, gpointer user_data) { SpawnData *data = user_data; g_subprocess_wait_check_finish (G_SUBPROCESS (obj), result, &data->error); spawn_data_exit (data); } 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 == '=')) return TRUE; arg++; } return FALSE; } char * flatpak_quote_argv (const char *argv[]) { GString *res = g_string_new (""); int i; for (i = 0; argv[i] != NULL; 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); } gboolean flatpak_spawn (GFile *dir, char **output, GError **error, const gchar *argv0, va_list ap) { GPtrArray *args; const gchar *arg; gboolean res; args = g_ptr_array_new (); g_ptr_array_add (args, (gchar *) argv0); while ((arg = va_arg (ap, const gchar *))) g_ptr_array_add (args, (gchar *) arg); g_ptr_array_add (args, NULL); res = flatpak_spawnv (dir, output, error, (const gchar * const *) args->pdata); g_ptr_array_free (args, TRUE); return res; } gboolean flatpak_spawnv (GFile *dir, char **output, GError **error, const gchar * const *argv) { g_autoptr(GSubprocessLauncher) launcher = NULL; g_autoptr(GSubprocess) subp = NULL; GInputStream *in; g_autoptr(GOutputStream) out = NULL; g_autoptr(GMainLoop) loop = NULL; SpawnData data = {0}; g_autofree gchar *commandline = NULL; launcher = g_subprocess_launcher_new (0); if (output) g_subprocess_launcher_set_flags (launcher, G_SUBPROCESS_FLAGS_STDOUT_PIPE); if (dir) { g_autofree char *path = g_file_get_path (dir); g_subprocess_launcher_set_cwd (launcher, path); } commandline = flatpak_quote_argv ((const char **)argv); g_debug ("Running: %s", commandline); subp = g_subprocess_launcher_spawnv (launcher, argv, error); if (subp == NULL) return FALSE; loop = g_main_loop_new (NULL, FALSE); data.loop = loop; data.refs = 1; if (output) { data.refs++; in = g_subprocess_get_stdout_pipe (subp); out = g_memory_output_stream_new_resizable (); g_output_stream_splice_async (out, in, G_OUTPUT_STREAM_SPLICE_NONE, 0, NULL, spawn_output_spliced_cb, &data); } g_subprocess_wait_async (subp, NULL, spawn_exit_cb, &data); g_main_loop_run (loop); if (data.error) { g_propagate_error (error, data.error); g_clear_error (&data.splice_error); return FALSE; } if (out) { if (data.splice_error) { g_propagate_error (error, data.splice_error); return FALSE; } /* Null terminate */ g_output_stream_write (out, "\0", 1, NULL, NULL); g_output_stream_close (out, NULL, NULL); *output = g_memory_output_stream_steal_data (G_MEMORY_OUTPUT_STREAM (out)); } return TRUE; } const char * flatpak_file_get_path_cached (GFile *file) { const char *path; static GQuark _file_path_quark = 0; if (G_UNLIKELY (_file_path_quark) == 0) _file_path_quark = g_quark_from_static_string ("flatpak-file-path"); do { path = g_object_get_qdata ((GObject*)file, _file_path_quark); if (path == NULL) { g_autofree char *new_path = NULL; new_path = g_file_get_path (file); if (new_path == NULL) return NULL; if (g_object_replace_qdata ((GObject*)file, _file_path_quark, NULL, new_path, g_free, NULL)) path = g_steal_pointer (&new_path); } } while (path == NULL); return path; } gboolean flatpak_openat_noatime (int dfd, const char *name, int *ret_fd, GCancellable *cancellable, GError **error) { int fd; int flags = O_RDONLY | O_CLOEXEC; #ifdef O_NOATIME do fd = openat (dfd, name, flags | O_NOATIME, 0); while (G_UNLIKELY (fd == -1 && errno == EINTR)); /* Only the owner or superuser may use O_NOATIME; so we may get * EPERM. EINVAL may happen if the kernel is really old... */ if (fd == -1 && (errno == EPERM || errno == EINVAL)) #endif do fd = openat (dfd, name, flags, 0); while (G_UNLIKELY (fd == -1 && errno == EINTR)); if (fd == -1) { glnx_set_error_from_errno (error); return FALSE; } else { *ret_fd = fd; return TRUE; } } #define COPY_BUFFER_SIZE (16*1024) gboolean flatpak_copy_bytes (int fdf, int fdt, GError **error) { char buf[COPY_BUFFER_SIZE]; int r; g_return_val_if_fail (fdf >= 0, FALSE); g_return_val_if_fail (fdt >= 0, FALSE); for (;;) { size_t m = COPY_BUFFER_SIZE; ssize_t n; n = read (fdf, buf, m); if (n < 0) { glnx_set_error_from_errno (error); return FALSE; } if (n == 0) /* EOF */ break; r = glnx_loop_write (fdt, buf, (size_t) n); if (r < 0) { errno = r; glnx_set_error_from_errno (error); return FALSE; } } 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; } gboolean flatpak_zero_mtime (int parent_dfd, const char *rel_path, GCancellable *cancellable, GError **error) { struct stat stbuf; 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, }; gboolean inited; inited = glnx_dirfd_iterator_init_at (parent_dfd, rel_path, FALSE, &dfd_iter, NULL); while (inited) { struct dirent *dent; if (!glnx_dirfd_iterator_next_dent (&dfd_iter, &dent, NULL, NULL) || dent == NULL) break; if (!flatpak_zero_mtime (dfd_iter.fd, dent->d_name, cancellable, error)) return FALSE; } /* Update stbuf */ if (TEMP_FAILURE_RETRY (fstat (dfd_iter.fd, &stbuf)) != 0) { glnx_set_error_from_errno (error); return FALSE; } } /* OSTree checks out to mtime 0, so we do the same */ if (stbuf.st_mtime != OSTREE_TIMESTAMP) { const struct timespec times[2] = { { 0, UTIME_OMIT }, { OSTREE_TIMESTAMP, } }; if (TEMP_FAILURE_RETRY (utimensat (parent_dfd, rel_path, times, AT_SYMLINK_NOFOLLOW)) != 0) { glnx_set_error_from_errno (error); return FALSE; } } return TRUE; } /* Make a directory, and its parent. Don't error if it already exists. * If you want a failure mode with EEXIST, use g_file_make_directory_with_parents. */ gboolean flatpak_mkdir_p (GFile *dir, GCancellable *cancellable, GError **error) { return glnx_shutil_mkdir_p_at (AT_FDCWD, flatpak_file_get_path_cached (dir), 0777, cancellable, error); } gboolean flatpak_rm_rf (GFile *dir, GCancellable *cancellable, GError **error) { return glnx_shutil_rm_rf_at (AT_FDCWD, flatpak_file_get_path_cached (dir), cancellable, error); } char * flatpak_readlink (const char *path, GError **error) { char buf[PATH_MAX + 1]; ssize_t symlink_size; symlink_size = readlink (path, buf, sizeof (buf) - 1); if (symlink_size < 0) { glnx_set_error_from_errno (error); return NULL; } buf[symlink_size] = 0; return g_strdup (buf); } char * flatpak_resolve_link (const char *path, GError **error) { g_autofree char *link = flatpak_readlink (path, error); g_autofree char *dirname = NULL; if (link == NULL) return NULL; if (g_path_is_absolute (link)) return g_steal_pointer (&link); dirname = g_path_get_dirname (path); return g_build_filename (dirname, link, NULL); } char * flatpak_canonicalize_filename (const char *path) { g_autoptr(GFile) file = g_file_new_for_path (path); return g_file_get_path (file); } 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; } 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; } GVariant * flatpak_gvariant_new_empty_string_dict (void) { g_auto(GVariantBuilder) builder = FLATPAK_VARIANT_BUILDER_INITIALIZER; g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}")); return g_variant_builder_end (&builder); } 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; } void flatpak_variant_builder_init_from_variant (GVariantBuilder *builder, const char *type, GVariant *variant) { gint i, n; g_variant_builder_init (builder, G_VARIANT_TYPE (type)); n = g_variant_n_children (variant); for (i = 0; i < n; i++) { GVariant *child = g_variant_get_child_value (variant, i); g_variant_builder_add_value (builder, child); g_variant_unref (child); } } gboolean flatpak_variant_bsearch_str (GVariant *array, const char *str, int *out_pos) { gsize imax, imin; gsize imid; gsize n; n = g_variant_n_children (array); if (n == 0) return FALSE; imax = n - 1; imin = 0; while (imax >= imin) { g_autoptr(GVariant) child = NULL; const char *cur; int cmp; imid = (imin + imax) / 2; child = g_variant_get_child_value (array, imid); g_variant_get_child (child, 0, "&s", &cur, NULL); cmp = strcmp (cur, str); if (cmp < 0) { imin = imid + 1; } else if (cmp > 0) { if (imid == 0) break; imax = imid - 1; } else { *out_pos = imid; return TRUE; } } *out_pos = imid; return FALSE; } /* This matches all refs that have ref, followed by '.' as prefix */ char ** flatpak_summary_match_subrefs (GVariant *summary, const char *ref) { g_autoptr(GVariant) refs = g_variant_get_child_value (summary, 0); GPtrArray *res = g_ptr_array_new (); gsize n, i; g_auto(GStrv) parts = NULL; g_autofree char *parts_prefix = NULL; parts = g_strsplit (ref, "/", 0); parts_prefix = g_strconcat (parts[1], ".", NULL); n = g_variant_n_children (refs); for (i = 0; i < n; i++) { g_autoptr(GVariant) child = NULL; g_auto(GStrv) cur_parts = NULL; const char *cur; child = g_variant_get_child_value (refs, i); g_variant_get_child (child, 0, "&s", &cur, NULL); cur_parts = g_strsplit (cur, "/", 0); /* Must match type, arch, branch */ if (strcmp (parts[0], cur_parts[0]) != 0 || strcmp (parts[2], cur_parts[2]) != 0 || strcmp (parts[3], cur_parts[3]) != 0) continue; /* But only prefix of id */ if (!g_str_has_prefix (cur_parts[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, const char *ref, char **out_checksum) { g_autoptr(GVariant) refs = g_variant_get_child_value (summary, 0); int pos; g_autoptr(GVariant) refdata = NULL; g_autoptr(GVariant) reftargetdata = NULL; guint64 commit_size; g_autoptr(GVariant) commit_csum_v = NULL; if (!flatpak_variant_bsearch_str (refs, ref, &pos)) return FALSE; refdata = g_variant_get_child_value (refs, pos); reftargetdata = g_variant_get_child_value (refdata, 1); g_variant_get (reftargetdata, "(t@ay@a{sv})", &commit_size, &commit_csum_v, NULL); if (!ostree_validate_structureof_csum_v (commit_csum_v, NULL)) return FALSE; if (out_checksum) *out_checksum = ostree_checksum_from_bytes_v (commit_csum_v); return TRUE; } 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_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; } 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) { if (!ostree_repo_query_object_storage_size (repo, OSTREE_OBJECT_TYPE_FILE, checksum, &obj_size, cancellable, error)) return FALSE; *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) { 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_fd_close 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; } CommitData; static void commit_data_free (gpointer data) { CommitData *rev_data = data; g_free (rev_data->metadata_contents); g_free (rev_data); } gboolean flatpak_repo_update (OstreeRepo *repo, const char **gpg_key_ids, const char *gpg_homedir, GCancellable *cancellable, GError **error) { GVariantBuilder builder; GVariantBuilder ref_data_builder; GKeyFile *config; g_autofree char *title = NULL; g_autofree char *default_branch = NULL; g_autoptr(GVariant) old_summary = NULL; g_autoptr(GVariant) new_summary = NULL; g_autoptr(GHashTable) refs = NULL; const char *prefixes[] = { "appstream", "app", "runtime", NULL }; const char **prefix; g_autoptr(GList) ordered_keys = NULL; GList *l = NULL; g_autoptr(GHashTable) commit_data_cache = NULL; g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT); config = ostree_repo_get_config (repo); if (config) { title = g_key_file_get_string (config, "flatpak", "title", NULL); default_branch = g_key_file_get_string (config, "flatpak", "default-branch", NULL); } if (title) g_variant_builder_add (&builder, "{sv}", "xa.title", g_variant_new_string (title)); if (default_branch) g_variant_builder_add (&builder, "{sv}", "xa.default-branch", g_variant_new_string (default_branch)); g_variant_builder_init (&ref_data_builder, G_VARIANT_TYPE ("a{s(tts)}")); /* 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 (old_summary != NULL) { g_autoptr(GVariant) extensions = g_variant_get_child_value (old_summary, 1); g_autoptr(GVariant) cache_v = g_variant_lookup_value (extensions, "xa.cache", NULL); g_autoptr(GVariant) cache = NULL; if (cache_v != NULL) { cache = g_variant_get_child_value (cache_v, 0); gsize n, i; 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 (old_summary, old_ref, &old_rev)) { 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_new (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); g_hash_table_insert (commit_data_cache, g_steal_pointer (&old_rev), old_rev_data); } } } } 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; CommitData *rev_data; /* 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, NULL, NULL, error)) return FALSE; 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); metadata = g_file_get_child (root, "metadata"); if (!g_file_load_contents (metadata, cancellable, &metadata_contents, NULL, NULL, NULL)) metadata_contents = g_strdup (""); rev_data = g_new (CommitData, 1); rev_data->installed_size = installed_size; rev_data->download_size = download_size; rev_data->metadata_contents = g_strdup (metadata_contents); 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); } g_variant_builder_add (&builder, "{sv}", "xa.cache", g_variant_new_variant (g_variant_builder_end (&ref_data_builder))); new_summary = g_variant_ref_sink (g_variant_builder_end (&builder)); 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; } gboolean flatpak_mtree_create_root (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 OstreeRepoCommitFilterResult commit_filter (OstreeRepo *repo, const char *path, GFileInfo *file_info, gpointer user_data) { guint current_mode; /* No user info */ g_file_info_set_attribute_uint32 (file_info, "unix::uid", 0); g_file_info_set_attribute_uint32 (file_info, "unix::gid", 0); /* No setuid */ current_mode = g_file_info_get_attribute_uint32 (file_info, "unix::mode"); g_file_info_set_attribute_uint32 (file_info, "unix::mode", current_mode & ~07000); return OSTREE_REPO_COMMIT_FILTER_ALLOW; } 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)); if (g_str_has_suffix (id_text, ".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 = "Application"; else group = "Runtime"; tags = g_key_file_get_string_list (metadata, group, "tags", NULL, NULL); runtime = g_key_file_get_string (metadata, group, "runtime", NULL); sdk = g_key_file_get_string (metadata, group, "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 *root, GFile *dest, const char *size, GError **error) { g_autofree char *icon_name = g_strconcat (id, ".png", NULL); g_autoptr(GFile) icons_dir = g_file_resolve_relative_path (root, "files/share/app-info/icons/flatpak"); 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); g_autoptr(GFile) dest_dir = g_file_get_child (dest, "icons"); g_autoptr(GFile) dest_size_dir = g_file_get_child (dest_dir, size); g_autoptr(GFile) dest_file = g_file_get_child (dest_size_dir, icon_name); g_autoptr(GInputStream) in = NULL; g_autoptr(GOutputStream) out = NULL; g_autoptr(GError) my_error = NULL; gssize n_bytes_written; in = (GInputStream *) g_file_read (icon_file, NULL, &my_error); if (!in) { if (g_error_matches (my_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { g_debug ("No icon at size %s", size); return TRUE; } g_propagate_error (error, g_steal_pointer (&my_error)); return FALSE; } if (!flatpak_mkdir_p (dest_size_dir, NULL, error)) return FALSE; out = (GOutputStream *) g_file_replace (dest_file, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION, NULL, error); if (!out) return FALSE; n_bytes_written = g_output_stream_splice (out, in, G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE, NULL, error); if (n_bytes_written < 0) return FALSE; return TRUE; } static gboolean extract_appstream (OstreeRepo *repo, FlatpakXml *appstream_root, const char *ref, const char *id, GFile *dest, GCancellable *cancellable, GError **error) { g_autoptr(GFile) root = NULL; g_autoptr(GFile) xmls_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; } xmls_dir = g_file_resolve_relative_path (root, "files/share/app-info/xmls"); 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; 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)); if (!g_str_has_prefix (component_id_text, id) || !g_str_has_suffix (component_id_text, ".desktop")) { component = component->next_sibling; continue; } g_print ("Extracting icons for component %s\n", component_id_text); component_id_text[strlen (component_id_text) - strlen (".desktop")] = 0; if (!copy_icon (component_id_text, root, dest, "64x64", &my_error)) { g_print ("Error copying 64x64 icon: %s\n", my_error->message); g_clear_error (&my_error); } if (!copy_icon (component_id_text, root, dest, "128x128", &my_error)) { g_print ("Error copying 128x128 icon: %s\n", my_error->message); g_clear_error (&my_error); } /* We updated icons for our component, so we're done */ break; } } 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; } GBytes * flatpak_appstream_xml_root_to_data (FlatpakXml *appstream_root, 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); 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 NULL; if (!g_output_stream_close (out2, NULL, error)) return NULL; return g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (out)); } gboolean flatpak_repo_generate_appstream (OstreeRepo *repo, const char **gpg_key_ids, const char *gpg_homedir, guint64 timestamp, GCancellable *cancellable, GError **error) { g_autoptr(GHashTable) all_refs = NULL; g_autoptr(GHashTable) arches = NULL; GHashTableIter iter; gpointer key; gpointer value; gboolean skip_commit = FALSE; arches = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); if (!ostree_repo_list_refs (repo, NULL, &all_refs, cancellable, error)) return FALSE; g_hash_table_iter_init (&iter, all_refs); while (g_hash_table_iter_next (&iter, &key, &value)) { const char *ref = key; const char *arch; g_auto(GStrv) split = NULL; split = flatpak_decompose_ref (ref, NULL); if (!split) continue; arch = split[2]; if (!g_hash_table_contains (arches, arch)) g_hash_table_insert (arches, g_strdup (arch), GINT_TO_POINTER (1)); } g_hash_table_iter_init (&iter, arches); while (g_hash_table_iter_next (&iter, &key, &value)) { GHashTableIter iter2; const char *arch = key; g_autofree char *tmpdir = g_strdup ("/tmp/flatpak-appstream-XXXXXX"); g_autoptr(FlatpakTempDir) tmpdir_file = NULL; g_autoptr(GFile) appstream_file = NULL; g_autoptr(GFile) root = NULL; g_autoptr(OstreeMutableTree) mtree = NULL; g_autofree char *commit_checksum = NULL; OstreeRepoTransactionStats stats; g_autoptr(OstreeRepoCommitModifier) modifier = NULL; g_autofree char *parent = NULL; g_autofree char *branch = NULL; g_autoptr(FlatpakXml) appstream_root = NULL; g_autoptr(GBytes) xml_data = NULL; if (g_mkdtemp_full (tmpdir, 0755) == NULL) return flatpak_fail (error, "Can't create temporary directory"); tmpdir_file = g_file_new_for_path (tmpdir); appstream_root = flatpak_appstream_xml_new (); g_hash_table_iter_init (&iter2, all_refs); while (g_hash_table_iter_next (&iter2, &key, &value)) { const char *ref = key; g_auto(GStrv) split = NULL; g_autoptr(GError) my_error = NULL; split = flatpak_decompose_ref (ref, NULL); if (!split) continue; if (strcmp (split[2], arch) != 0) continue; if (!extract_appstream (repo, appstream_root, ref, split[1], tmpdir_file, cancellable, &my_error)) { if (g_str_has_prefix (ref, "app/")) g_print ("No appstream data for %s: %s\n", ref, my_error->message); continue; } } xml_data = flatpak_appstream_xml_root_to_data (appstream_root, error); if (xml_data == NULL) return FALSE; appstream_file = g_file_get_child (tmpdir_file, "appstream.xml.gz"); if (!g_file_replace_contents (appstream_file, g_bytes_get_data (xml_data, NULL), g_bytes_get_size (xml_data), NULL, FALSE, G_FILE_CREATE_NONE, NULL, cancellable, error)) return FALSE; if (!ostree_repo_prepare_transaction (repo, NULL, cancellable, error)) return FALSE; branch = g_strdup_printf ("appstream/%s", arch); if (!ostree_repo_resolve_rev (repo, branch, TRUE, &parent, error)) goto out; mtree = ostree_mutable_tree_new (); modifier = ostree_repo_commit_modifier_new (OSTREE_REPO_COMMIT_MODIFIER_FLAGS_SKIP_XATTRS, (OstreeRepoCommitFilter) commit_filter, NULL, NULL); if (!ostree_repo_write_directory_to_mtree (repo, G_FILE (tmpdir_file), mtree, modifier, cancellable, error)) goto out; if (!ostree_repo_write_mtree (repo, mtree, &root, cancellable, error)) goto out; /* No need to commit if nothing changed */ if (parent) { g_autoptr(GFile) parent_root; if (!ostree_repo_read_commit (repo, parent, &parent_root, NULL, cancellable, error)) goto out; if (g_file_equal (root, parent_root)) skip_commit = TRUE; } if (!skip_commit) { if (timestamp > 0) { if (!ostree_repo_write_commit_with_time (repo, parent, "Update", NULL, NULL, OSTREE_REPO_FILE (root), timestamp, &commit_checksum, cancellable, error)) goto out; } else { if (!ostree_repo_write_commit (repo, parent, "Update", NULL, NULL, OSTREE_REPO_FILE (root), &commit_checksum, cancellable, error)) goto out; } 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)) goto out; } } ostree_repo_transaction_set_ref (repo, NULL, branch, commit_checksum); if (!ostree_repo_commit_transaction (repo, &stats, cancellable, error)) goto out; } else { ostree_repo_abort_transaction (repo, cancellable, NULL); } } return TRUE; out: ostree_repo_abort_transaction (repo, cancellable, NULL); return FALSE; } void flatpak_extension_free (FlatpakExtension *extension) { g_free (extension->id); g_free (extension->installed_id); 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, gboolean is_unmaintained) { FlatpakExtension *ext = g_new0 (FlatpakExtension, 1); 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 (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, "ExtensionOf", "priority", NULL); } return ext; } gboolean flatpak_extension_matches_reason (const char *extension_id, const char *reason, gboolean default_value) { const char *extension_basename; if (reason == NULL || *reason == 0) return default_value; extension_basename = strrchr (extension_id, '.'); if (extension_basename == NULL) return FALSE; extension_basename += 1; if (strcmp (reason, "active-gl-driver") == 0) { /* handled below */ const char **gl_drivers = flatpak_get_gl_drivers (); int i; for (i = 0; gl_drivers[i] != NULL; i++) { if (strcmp (gl_drivers[i], extension_basename) == 0) return TRUE; } return FALSE; } 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, "directory", NULL); g_autofree char *add_ld_path = g_key_file_get_string (metakey, group, "add-ld-path", NULL); g_auto(GStrv) merge_dirs = g_key_file_get_string_list (metakey, group, "merge-dirs", NULL, NULL); g_autofree char *enable_if = g_key_file_get_string (metakey, group, "enable-if", NULL); g_autofree char *subdir_suffix = g_key_file_get_string (metakey, group, "subdirectory-suffix", NULL); g_autofree char *ref = NULL; gboolean is_unmaintained = FALSE; g_autoptr(GFile) files = 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) files = flatpak_find_files_dir_for_ref (ref, NULL, NULL); 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, is_unmaintained); res = g_list_prepend (res, ext); } } else if (g_key_file_get_boolean (metakey, group, "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_files = flatpak_find_files_dir_for_ref (dir_ref, NULL, NULL); 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, 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, TRUE); ext->needs_tmpfs = TRUE; res = g_list_prepend (res, ext); } } } return res; } 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], "Extension ") && *(extension = (groups[i] + strlen ("Extension "))) != 0) { g_autofree char *version = g_key_file_get_string (metakey, groups[i], "version", NULL); g_auto(GStrv) versions = g_key_file_get_string_list (metakey, groups[i], "versions", NULL, NULL); const char *default_branches[] = { default_branch, NULL}; const char **branches; 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], extension, 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, "\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, "", 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); g_print ("Number of parts: %u\n", n_parts); 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, 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, "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 (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; metadata = flatpak_bundle_load (file, &to_checksum, NULL, NULL, NULL, &metadata_contents, NULL, NULL, error); if (metadata == NULL) return FALSE; if (!ostree_repo_prepare_transaction (repo, NULL, cancellable, error)) return FALSE; 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) { /* NOT_FOUND means 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, G_IO_ERROR, G_IO_ERROR_NOT_FOUND) && !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, "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, "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); } char * flatpak_pull_from_oci (OstreeRepo *repo, FlatpakOciRegistry *registry, const char *digest, FlatpakOciManifest *manifest, const char *ref, FlatpakOciPullProgress progress_cb, gpointer progress_user_data, GCancellable *cancellable, GError **error) { g_autoptr(OstreeMutableTree) archive_mtree = NULL; g_autoptr(GFile) archive_root = NULL; g_autofree char *commit_checksum = NULL; const char *parent = NULL; g_autofree char *subject = NULL; g_autofree char *body = NULL; 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 *annotations; int i; g_assert (ref != NULL); g_assert (g_str_has_prefix (digest, "sha256:")); annotations = flatpak_oci_manifest_get_annotations (manifest); if (annotations) flatpak_oci_parse_commit_annotations (annotations, ×tamp, &subject, &body, NULL, NULL, NULL, metadata_builder); g_variant_builder_add (metadata_builder, "{s@v}", "xa.alt-id", g_variant_new_variant (g_variant_new_string (digest + strlen("sha256:")))); 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]; 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]; OstreeRepoImportArchiveOptions opts = { 0, }; free_read_archive struct archive *a = NULL; glnx_fd_close int layer_fd = -1; opts.autocreate_parents = TRUE; opts.ignore_unsupported_content = TRUE; layer_fd = flatpak_oci_registry_download_blob (registry, layer->digest, oci_layer_progress, &progress_data, cancellable, error); if (layer_fd == -1) goto error; 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 (archive_read_open_fd (a, layer_fd, 8192) != ARCHIVE_OK) { propagate_libarchive_error (error, a); 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; } progress_data.pulled_layers++; progress_data.previous_layers_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; ostree_repo_transaction_set_ref (repo, NULL, 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_fd_close 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_fd_close 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); glnx_fd_close int new_tmpdir_fd = -1; g_autoptr(GError) local_error = NULL; g_autofree char *lock_name = NULL; /* No existing tmpdir found, create a new */ if (!glnx_mkdtempat (dfd_iter.fd, tmpdir_name_template, 0777, error)) return FALSE; if (!glnx_opendirat (dfd_iter.fd, tmpdir_name_template, FALSE, &new_tmpdir_fd, error)) return FALSE; lock_name = g_strconcat (tmpdir_name_template, "-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)) { continue; } else { g_propagate_error (error, g_steal_pointer (&local_error)); return FALSE; } } tmpdir_name = g_steal_pointer (&tmpdir_name_template); tmpdir_fd = glnx_steal_fd (&new_tmpdir_fd); } if (tmpdir_name_out) *tmpdir_name_out = g_steal_pointer (&tmpdir_name); if (tmpdir_fd_out) *tmpdir_fd_out = glnx_steal_fd (&tmpdir_fd); if (reusing_dir_out) *reusing_dir_out = reusing_dir; return TRUE; } gboolean flatpak_yes_no_prompt (const char *prompt, ...) { char buf[512]; va_list var_args; gchar *s; #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, "[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 (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) { while (*s != 0) { if (!g_ascii_isdigit (*s)) return FALSE; s++; } return TRUE; } long flatpak_number_prompt (int min, int max, const char *prompt, ...) { char buf[512]; va_list var_args; gchar *s; 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 (is_number (buf)) { long res = strtol (buf, NULL, 10); if (res >= min && res <= max) return res; } } } typedef struct { GMainLoop *loop; GError *error; GOutputStream *out; guint64 downloaded_bytes; GString *content; char buffer[16*1024]; FlatpakLoadUriProgress progress; GCancellable *cancellable; gpointer user_data; guint64 last_progress_time; char *etag; } LoadUriData; static void stream_closed (GObject *source, GAsyncResult *res, gpointer user_data) { LoadUriData *data = user_data; GInputStream *stream = G_INPUT_STREAM (source); g_autoptr(GError) error = NULL; if (!g_input_stream_close_finish (stream, res, &error)) g_warning ("Error closing http stream: %s\n", error->message); g_main_loop_quit (data->loop); } static void load_uri_read_cb (GObject *source, GAsyncResult *res, gpointer user_data) { LoadUriData *data = user_data; GInputStream *stream = G_INPUT_STREAM (source); gsize nread; nread = g_input_stream_read_finish (stream, res, &data->error); if (nread == -1 || nread == 0) { if (data->progress) data->progress (data->downloaded_bytes, data->user_data); g_input_stream_close_async (stream, G_PRIORITY_DEFAULT, NULL, stream_closed, data); return; } if (data->out != NULL) { gsize n_written; if (!g_output_stream_write_all (data->out, data->buffer, nread, &n_written, NULL, &data->error)) { data->downloaded_bytes += n_written; g_input_stream_close_async (stream, G_PRIORITY_DEFAULT, NULL, stream_closed, data); return; } data->downloaded_bytes += n_written; } else { data->downloaded_bytes += nread; g_string_append_len (data->content, data->buffer, nread); } if (g_get_monotonic_time () - data->last_progress_time > 1 * G_USEC_PER_SEC) { if (data->progress) data->progress (data->downloaded_bytes, data->user_data); data->last_progress_time = g_get_monotonic_time (); } g_input_stream_read_async (stream, data->buffer, sizeof (data->buffer), G_PRIORITY_DEFAULT, data->cancellable, load_uri_read_cb, data); } static void load_uri_callback (GObject *source_object, GAsyncResult *res, gpointer user_data) { SoupRequestHTTP *request = SOUP_REQUEST_HTTP(source_object); g_autoptr(GInputStream) in = NULL; LoadUriData *data = user_data; in = soup_request_send_finish (SOUP_REQUEST (request), res, &data->error); if (in == NULL) { g_main_loop_quit (data->loop); return; } g_autoptr(SoupMessage) msg = soup_request_http_get_message ((SoupRequestHTTP*) request); if (!SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) { int code; GQuark domain = G_IO_ERROR; switch (msg->status_code) { case 304: domain = FLATPAK_OCI_ERROR; code = FLATPAK_OCI_ERROR_NOT_CHANGED; break; case 404: case 410: code = G_IO_ERROR_NOT_FOUND; break; default: code = G_IO_ERROR_FAILED; } data->error = g_error_new (domain, code, "Server returned status %u: %s", msg->status_code, soup_status_get_phrase (msg->status_code)); g_main_loop_quit (data->loop); return; } data->etag = g_strdup (soup_message_headers_get_one (msg->response_headers, "ETag")); g_input_stream_read_async (in, data->buffer, sizeof (data->buffer), G_PRIORITY_DEFAULT, data->cancellable, load_uri_read_cb, data); } SoupSession * flatpak_create_soup_session (const char *user_agent) { SoupSession *soup_session; const char *http_proxy; soup_session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, user_agent, SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE, SOUP_SESSION_USE_THREAD_CONTEXT, TRUE, SOUP_SESSION_TIMEOUT, 60, SOUP_SESSION_IDLE_TIMEOUT, 60, NULL); soup_session_remove_feature_by_type (soup_session, SOUP_TYPE_CONTENT_DECODER); http_proxy = g_getenv ("http_proxy"); if (http_proxy) { g_autoptr(SoupURI) proxy_uri = soup_uri_new (http_proxy); if (!proxy_uri) g_warning ("Invalid proxy URI '%s'", http_proxy); else g_object_set (soup_session, SOUP_SESSION_PROXY_URI, proxy_uri, NULL); } return soup_session; } GBytes * flatpak_load_http_uri (SoupSession *soup_session, const char *uri, const char *etag, char **out_etag, FlatpakLoadUriProgress progress, gpointer user_data, GCancellable *cancellable, GError **error) { GBytes *bytes = NULL; g_autoptr(GMainContext) context = NULL; g_autoptr(SoupRequestHTTP) request = NULL; g_autoptr(GMainLoop) loop = NULL; g_autoptr(GString) content = g_string_new (""); LoadUriData data = { NULL }; g_debug ("Loading %s using libsoup", uri); context = g_main_context_ref_thread_default (); loop = g_main_loop_new (context, TRUE); data.loop = loop; data.content = content; data.progress = progress; data.cancellable = cancellable; data.user_data = user_data; data.last_progress_time = g_get_monotonic_time (); request = soup_session_request_http (soup_session, "GET", uri, error); if (request == NULL) return NULL; if (etag) { SoupMessage *m = soup_request_http_get_message (request); soup_message_headers_replace (m->request_headers, "If-None-Match", etag); } soup_request_send_async (SOUP_REQUEST(request), cancellable, load_uri_callback, &data); g_main_loop_run (loop); if (data.error) { g_propagate_error (error, data.error); g_free (data.etag); return NULL; } bytes = g_string_free_to_bytes (g_steal_pointer (&content)); g_debug ("Received %" G_GSIZE_FORMAT " bytes", data.downloaded_bytes); if (out_etag) *out_etag = g_steal_pointer (&data.etag); g_free (data.etag); return bytes; } gboolean flatpak_download_http_uri (SoupSession *soup_session, const char *uri, GOutputStream *out, FlatpakLoadUriProgress progress, gpointer user_data, GCancellable *cancellable, GError **error) { g_autoptr(SoupRequestHTTP) request = NULL; g_autoptr(GMainLoop) loop = NULL; g_autoptr(GMainContext) context = NULL; LoadUriData data = { NULL }; g_debug ("Loading %s using libsoup", uri); context = g_main_context_ref_thread_default (); loop = g_main_loop_new (context, TRUE); data.loop = loop; data.out = out; data.progress = progress; data.cancellable = cancellable; data.user_data = user_data; data.last_progress_time = g_get_monotonic_time (); request = soup_session_request_http (soup_session, "GET", uri, error); if (request == NULL) return FALSE; soup_request_send_async (SOUP_REQUEST(request), cancellable, load_uri_callback, &data); g_main_loop_run (loop); if (data.error) { g_propagate_error (error, data.error); return FALSE; } g_debug ("Received %" G_GSIZE_FORMAT " bytes", data.downloaded_bytes); return TRUE; } /* Uncomment to get debug traces in /tmp/flatpak-completion-debug.txt (nice * to not have it interfere with stdout/stderr) */ #if 0 void flatpak_completion_debug (const gchar *format, ...) { va_list var_args; gchar *s; static FILE *f = NULL; va_start (var_args, format); s = g_strdup_vprintf (format, var_args); if (f == NULL) f = fopen ("/tmp/flatpak-completion-debug.txt", "a+"); fprintf (f, "%s\n", s); fflush (f); g_free (s); } #else void flatpak_completion_debug (const gchar *format, ...) { } #endif static gboolean is_word_separator (char c) { return g_ascii_isspace (c); } void flatpak_complete_file (FlatpakCompletion *completion) { flatpak_completion_debug ("completing FILE"); g_print ("%s\n", "__FLATPAK_FILE"); } void flatpak_complete_dir (FlatpakCompletion *completion) { flatpak_completion_debug ("completing DIR"); g_print ("%s\n", "__FLATPAK_DIR"); } void flatpak_complete_word (FlatpakCompletion *completion, char *format, ...) { va_list args; const char *rest; const char *shell_cur; g_autofree char *string = NULL; g_return_if_fail (format != NULL); #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wformat-nonliteral" va_start (args, format); string = g_strdup_vprintf (format, args); va_end (args); #pragma GCC diagnostic pop if (!g_str_has_prefix (string, completion->cur)) return; shell_cur = completion->shell_cur ? completion->shell_cur : ""; /* I'm not sure exactly what bash is doing here, but this seems to work... */ if (strcmp (shell_cur, "=") == 0) rest = string + strlen (completion->cur) - strlen (shell_cur) + 1; else rest = string + strlen (completion->cur) - strlen (shell_cur); flatpak_completion_debug ("completing word: '%s' (%s)", string, rest); g_print ("%s\n", rest); } void flatpak_complete_ref (FlatpakCompletion *completion, OstreeRepo *repo) { g_autoptr(GHashTable) refs = NULL; flatpak_completion_debug ("completing REF"); if (ostree_repo_list_refs (repo, NULL, &refs, NULL, NULL)) { GHashTableIter hashiter; gpointer hashkey, hashvalue; g_hash_table_iter_init (&hashiter, refs); while ((g_hash_table_iter_next (&hashiter, &hashkey, &hashvalue))) { const char *ref = (const char *)hashkey; if (!(g_str_has_prefix (ref, "runtime/") || g_str_has_prefix (ref, "app/"))) continue; flatpak_complete_word (completion, "%s", ref); } } } static int find_current_element (const char *str) { int count = 0; if (g_str_has_prefix (str, "app/")) str += strlen ("app/"); else if (g_str_has_prefix (str, "runtime/")) str += strlen ("runtime/"); while (str != NULL && count <= 3) { str = strchr (str, '/'); count++; if (str != NULL) str = str + 1; } return count; } void flatpak_complete_partial_ref (FlatpakCompletion *completion, FlatpakKinds kinds, const char *only_arch, FlatpakDir *dir, const char *remote) { FlatpakKinds matched_kinds; const char *pref; g_autofree char *id = NULL; g_autofree char *arch = NULL; g_autofree char *branch = NULL; g_auto(GStrv) refs = NULL; int element; const char *cur_parts[4] = { NULL }; g_autoptr(GError) error = NULL; int i; pref = completion->cur; element = find_current_element (pref); flatpak_split_partial_ref_arg_novalidate (pref, kinds, NULL, NULL, &matched_kinds, &id, &arch, &branch); cur_parts[1] = id; cur_parts[2] = arch ? arch : ""; cur_parts[3] = branch ? branch : ""; if (remote) { refs = flatpak_dir_find_remote_refs (dir, completion->argv[1], (element > 1) ? id : NULL, (element > 3) ? branch : NULL, (element > 2 )? arch : only_arch, matched_kinds, NULL, &error); } else { refs = flatpak_dir_find_installed_refs (dir, (element > 1) ? id : NULL, (element > 3) ? branch : NULL, (element > 2 )? arch : only_arch, matched_kinds, &error); } if (refs == NULL) flatpak_completion_debug ("find refs error: %s", error->message); for (i = 0; refs != NULL && refs[i] != NULL; i++) { int j; g_autoptr(GString) comp = NULL; g_auto(GStrv) parts = flatpak_decompose_ref (refs[i], NULL); if (parts == NULL) continue; if (!g_str_has_prefix (parts[element], cur_parts[element])) continue; comp = g_string_new (pref); g_string_append (comp, parts[element] + strlen (cur_parts[element])); /* Only complete on the last part if the user explicitly adds a / */ if (element >= 2) { for (j = element + 1; j < 4; j++) { g_string_append (comp, "/"); g_string_append (comp, parts[j]); } } flatpak_complete_word (completion, "%s", comp->str); } } static gboolean switch_already_in_line (FlatpakCompletion *completion, GOptionEntry *entry) { guint i = 0; guint line_part_len = 0; for (; i < completion->original_argc; ++i) { line_part_len = strlen (completion->original_argv[i]); if (line_part_len > 2 && g_strcmp0 (&completion->original_argv[i][2], entry->long_name) == 0) return TRUE; if (line_part_len == 2 && completion->original_argv[i][1] == entry->short_name) return TRUE; } return FALSE; } static gboolean should_filter_out_option_from_completion (FlatpakCompletion *completion, GOptionEntry *entry) { switch (entry->arg) { case G_OPTION_ARG_NONE: case G_OPTION_ARG_STRING: case G_OPTION_ARG_INT: case G_OPTION_ARG_FILENAME: case G_OPTION_ARG_DOUBLE: case G_OPTION_ARG_INT64: return switch_already_in_line (completion, entry); default: return FALSE; } } void flatpak_complete_options (FlatpakCompletion *completion, GOptionEntry *entries) { GOptionEntry *e = entries; int i; while (e->long_name != NULL) { if (e->arg_description) { g_autofree char *prefix = g_strdup_printf ("--%s=", e->long_name); if (g_str_has_prefix (completion->cur, prefix)) { if (strcmp (e->arg_description, "ARCH") == 0) { const char *arches[] = {"i386", "x86_64", "aarch64", "arm"}; for (i = 0; i < G_N_ELEMENTS (arches); i++) flatpak_complete_word (completion, "%s%s ", prefix, arches[i]); } else if (strcmp (e->arg_description, "SHARE") == 0) { for (i = 0; flatpak_context_shares[i] != NULL; i++) flatpak_complete_word (completion, "%s%s ", prefix, flatpak_context_shares[i]); } else if (strcmp (e->arg_description, "DEVICE") == 0) { for (i = 0; flatpak_context_devices[i] != NULL; i++) flatpak_complete_word (completion, "%s%s ", prefix, flatpak_context_devices[i]); } else if (strcmp (e->arg_description, "FEATURE") == 0) { for (i = 0; flatpak_context_features[i] != NULL; i++) flatpak_complete_word (completion, "%s%s ", prefix, flatpak_context_features[i]); } else if (strcmp (e->arg_description, "SOCKET") == 0) { for (i = 0; flatpak_context_sockets[i] != NULL; i++) flatpak_complete_word (completion, "%s%s ", prefix, flatpak_context_sockets[i]); } else if (strcmp (e->arg_description, "FILE") == 0) { flatpak_complete_file (completion); } else flatpak_complete_word (completion, "%s", prefix); } else flatpak_complete_word (completion, "%s", prefix); } else { /* If this is just a switch, then don't add it multiple * times */ if (!should_filter_out_option_from_completion (completion, e)) { flatpak_complete_word (completion, "--%s ", e->long_name); } else { flatpak_completion_debug ("switch --%s is already in line %s", e->long_name, completion->line); } } /* We may end up checking switch_already_in_line twice, but this is * for simplicity's sake - the alternative solution would be to * continue the loop early and have to increment e. */ if (e->short_name != 0) { /* This is a switch, we may not want to add it */ if (!e->arg_description) { if (!should_filter_out_option_from_completion (completion, e)) { flatpak_complete_word (completion, "-%c ", e->short_name); } else { flatpak_completion_debug ("switch -%c is already in line %s", e->short_name, completion->line); } } else { flatpak_complete_word (completion, "-%c ", e->short_name); } } e++; } } static gchar * pick_word_at (const char *s, int cursor, int *out_word_begins_at) { int begin, end; if (s[0] == '\0') { if (out_word_begins_at != NULL) *out_word_begins_at = -1; return NULL; } if (is_word_separator (s[cursor]) && ((cursor > 0 && is_word_separator(s[cursor-1])) || cursor == 0)) { if (out_word_begins_at != NULL) *out_word_begins_at = cursor; return g_strdup (""); } while (!is_word_separator (s[cursor - 1]) && cursor > 0) cursor--; begin = cursor; end = begin; while (!is_word_separator (s[end]) && s[end] != '\0') end++; if (out_word_begins_at != NULL) *out_word_begins_at = begin; return g_strndup (s + begin, end - begin); } static gboolean parse_completion_line_to_argv (const char *initial_completion_line, FlatpakCompletion *completion) { gboolean parse_result = g_shell_parse_argv (initial_completion_line, &completion->original_argc, &completion->original_argv, NULL); /* Make a shallow copy of argv, which will be our "working set" */ completion->argc = completion->original_argc; completion->argv = g_memdup (completion->original_argv, sizeof (gchar *) * (completion->original_argc + 1)); return parse_result; } FlatpakCompletion * flatpak_completion_new (const char *arg_line, const char *arg_point, const char *arg_cur) { FlatpakCompletion *completion; g_autofree char *initial_completion_line = NULL; int _point; char *endp; int cur_begin; int i; _point = strtol (arg_point, &endp, 10); if (endp == arg_point || *endp != '\0') return NULL; completion = g_new0 (FlatpakCompletion, 1); completion->line = g_strdup (arg_line); completion->shell_cur = g_strdup (arg_cur); completion->point = _point; flatpak_completion_debug ("========================================"); flatpak_completion_debug ("completion_point=%d", completion->point); flatpak_completion_debug ("completion_shell_cur='%s'", completion->shell_cur); flatpak_completion_debug ("----"); flatpak_completion_debug (" 0123456789012345678901234567890123456789012345678901234567890123456789"); flatpak_completion_debug ("'%s'", completion->line); flatpak_completion_debug (" %*s^", completion->point, ""); /* compute cur and prev */ completion->prev = NULL; completion->cur = pick_word_at (completion->line, completion->point, &cur_begin); if (cur_begin > 0) { gint prev_end; for (prev_end = cur_begin - 1; prev_end >= 0; prev_end--) { if (!is_word_separator (completion->line[prev_end])) { completion->prev = pick_word_at (completion->line, prev_end, NULL); break; } } initial_completion_line = g_strndup (completion->line, cur_begin); } else initial_completion_line = g_strdup (""); flatpak_completion_debug ("'%s'", initial_completion_line); flatpak_completion_debug ("----"); flatpak_completion_debug (" cur='%s'", completion->cur); flatpak_completion_debug ("prev='%s'", completion->prev); if (!parse_completion_line_to_argv (initial_completion_line, completion)) { /* it's very possible the command line can't be parsed (for * example, missing quotes etc) - in that case, we just * don't autocomplete at all */ flatpak_completion_free (completion); return NULL; } flatpak_completion_debug ("completion_argv %i:", completion->original_argc); for (i = 0; i < completion->original_argc; i++) flatpak_completion_debug (completion->original_argv[i]); flatpak_completion_debug ("----"); return completion; } void flatpak_completion_free (FlatpakCompletion *completion) { g_free (completion->cur); g_free (completion->prev); g_free (completion->line); g_free (completion->argv); g_strfreev (completion->original_argv); g_free (completion); } char ** flatpak_get_current_locale_subpaths (void) { const gchar * const *langs = g_get_language_names (); GPtrArray *subpaths = g_ptr_array_new (); int i; for (i = 0; langs[i] != NULL; i++) { g_autofree char *dir = g_strconcat ("/", langs[i], NULL); char *c; c = strchr (dir, '@'); if (c != NULL) *c = 0; c = strchr (dir, '_'); if (c != NULL) *c = 0; c = strchr (dir, '.'); if (c != NULL) *c = 0; if (strcmp (dir, "/C") == 0) continue; g_ptr_array_add (subpaths, g_steal_pointer (&dir)); } g_ptr_array_add (subpaths, NULL); return (char **)g_ptr_array_free (subpaths, FALSE); }