mirror of
https://github.com/flatpak/flatpak.git
synced 2026-01-01 04:18:05 -05:00
In order to reuse in flatpak-oci-signatures.c, move: local_open_file => flatpak_open_file_at local_load_file => flatpak_load_file_at
2604 lines
71 KiB
C
2604 lines
71 KiB
C
/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s:
|
|
* Copyright © 1995-1998 Free Software Foundation, Inc.
|
|
* Copyright © 2014-2019 Red Hat, Inc
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* Authors:
|
|
* Alexander Larsson <alexl@redhat.com>
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <glib/gi18n-lib.h>
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <errno.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/file.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/types.h>
|
|
#include <sys/utsname.h>
|
|
#include <sys/ioctl.h>
|
|
#include <termios.h>
|
|
|
|
#include <glib.h>
|
|
#include <gio/gunixoutputstream.h>
|
|
|
|
#include "flatpak-error.h"
|
|
#include "flatpak-utils-base-private.h"
|
|
#include "flatpak-utils-private.h"
|
|
#include "libglnx.h"
|
|
#include "valgrind-private.h"
|
|
|
|
/* This is also here so the common code can report these errors to the lib */
|
|
static const GDBusErrorEntry flatpak_error_entries[] = {
|
|
{FLATPAK_ERROR_ALREADY_INSTALLED, "org.freedesktop.Flatpak.Error.AlreadyInstalled"},
|
|
{FLATPAK_ERROR_NOT_INSTALLED, "org.freedesktop.Flatpak.Error.NotInstalled"},
|
|
{FLATPAK_ERROR_ONLY_PULLED, "org.freedesktop.Flatpak.Error.OnlyPulled"}, /* Since: 1.0 */
|
|
{FLATPAK_ERROR_DIFFERENT_REMOTE, "org.freedesktop.Flatpak.Error.DifferentRemote"}, /* Since: 1.0 */
|
|
{FLATPAK_ERROR_ABORTED, "org.freedesktop.Flatpak.Error.Aborted"}, /* Since: 1.0 */
|
|
{FLATPAK_ERROR_SKIPPED, "org.freedesktop.Flatpak.Error.Skipped"}, /* Since: 1.0 */
|
|
{FLATPAK_ERROR_NEED_NEW_FLATPAK, "org.freedesktop.Flatpak.Error.NeedNewFlatpak"}, /* Since: 1.0 */
|
|
{FLATPAK_ERROR_REMOTE_NOT_FOUND, "org.freedesktop.Flatpak.Error.RemoteNotFound"}, /* Since: 1.0 */
|
|
{FLATPAK_ERROR_RUNTIME_NOT_FOUND, "org.freedesktop.Flatpak.Error.RuntimeNotFound"}, /* Since: 1.0 */
|
|
{FLATPAK_ERROR_DOWNGRADE, "org.freedesktop.Flatpak.Error.Downgrade"}, /* Since: 1.0 */
|
|
{FLATPAK_ERROR_INVALID_REF, "org.freedesktop.Flatpak.Error.InvalidRef"}, /* Since: 1.0.3 */
|
|
{FLATPAK_ERROR_INVALID_DATA, "org.freedesktop.Flatpak.Error.InvalidData"}, /* Since: 1.0.3 */
|
|
{FLATPAK_ERROR_UNTRUSTED, "org.freedesktop.Flatpak.Error.Untrusted"}, /* Since: 1.0.3 */
|
|
{FLATPAK_ERROR_SETUP_FAILED, "org.freedesktop.Flatpak.Error.SetupFailed"}, /* Since: 1.0.3 */
|
|
{FLATPAK_ERROR_EXPORT_FAILED, "org.freedesktop.Flatpak.Error.ExportFailed"}, /* Since: 1.0.3 */
|
|
{FLATPAK_ERROR_REMOTE_USED, "org.freedesktop.Flatpak.Error.RemoteUsed"}, /* Since: 1.0.3 */
|
|
{FLATPAK_ERROR_RUNTIME_USED, "org.freedesktop.Flatpak.Error.RuntimeUsed"}, /* Since: 1.0.3 */
|
|
{FLATPAK_ERROR_INVALID_NAME, "org.freedesktop.Flatpak.Error.InvalidName"}, /* Since: 1.0.3 */
|
|
{FLATPAK_ERROR_OUT_OF_SPACE, "org.freedesktop.Flatpak.Error.OutOfSpace"}, /* Since: 1.2.0 */
|
|
{FLATPAK_ERROR_WRONG_USER, "org.freedesktop.Flatpak.Error.WrongUser"}, /* Since: 1.2.0 */
|
|
{FLATPAK_ERROR_NOT_CACHED, "org.freedesktop.Flatpak.Error.NotCached"}, /* Since: 1.3.3 */
|
|
{FLATPAK_ERROR_REF_NOT_FOUND, "org.freedesktop.Flatpak.Error.RefNotFound"}, /* Since: 1.4.0 */
|
|
{FLATPAK_ERROR_PERMISSION_DENIED, "org.freedesktop.Flatpak.Error.PermissionDenied"}, /* Since: 1.5.1 */
|
|
{FLATPAK_ERROR_AUTHENTICATION_FAILED, "org.freedesktop.Flatpak.Error.AuthenticationFailed"}, /* Since: 1.7.3 */
|
|
{FLATPAK_ERROR_NOT_AUTHORIZED, "org.freedesktop.Flatpak.Error.NotAuthorized"}, /* Since: 1.7.3 */
|
|
};
|
|
|
|
GQuark
|
|
flatpak_error_quark (void)
|
|
{
|
|
static volatile gsize quark_volatile = 0;
|
|
|
|
g_dbus_error_register_error_domain ("flatpak-error-quark",
|
|
&quark_volatile,
|
|
flatpak_error_entries,
|
|
G_N_ELEMENTS (flatpak_error_entries));
|
|
return (GQuark) quark_volatile;
|
|
}
|
|
|
|
gboolean
|
|
flatpak_fail_error (GError **error, FlatpakError code, const char *fmt, ...)
|
|
{
|
|
if (error == NULL)
|
|
return FALSE;
|
|
|
|
va_list args;
|
|
va_start (args, fmt);
|
|
GError *new = g_error_new_valist (FLATPAK_ERROR, code, fmt, args);
|
|
va_end (args);
|
|
g_propagate_error (error, g_steal_pointer (&new));
|
|
return FALSE;
|
|
}
|
|
|
|
GBytes *
|
|
flatpak_zlib_compress_bytes (GBytes *bytes,
|
|
int level,
|
|
GError **error)
|
|
{
|
|
g_autoptr(GZlibCompressor) compressor = NULL;
|
|
g_autoptr(GOutputStream) out = NULL;
|
|
g_autoptr(GOutputStream) mem = NULL;
|
|
|
|
mem = g_memory_output_stream_new_resizable ();
|
|
|
|
compressor = g_zlib_compressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP, level);
|
|
out = g_converter_output_stream_new (mem, G_CONVERTER (compressor));
|
|
|
|
if (!g_output_stream_write_all (out, g_bytes_get_data (bytes, NULL), g_bytes_get_size (bytes),
|
|
NULL, NULL, error))
|
|
return NULL;
|
|
|
|
if (!g_output_stream_close (out, NULL, error))
|
|
return NULL;
|
|
|
|
return g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (mem));
|
|
}
|
|
|
|
GBytes *
|
|
flatpak_zlib_decompress_bytes (GBytes *bytes,
|
|
GError **error)
|
|
{
|
|
g_autoptr(GZlibDecompressor) decompressor = NULL;
|
|
g_autoptr(GOutputStream) out = NULL;
|
|
g_autoptr(GOutputStream) mem = NULL;
|
|
|
|
mem = g_memory_output_stream_new_resizable ();
|
|
|
|
decompressor = g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP);
|
|
out = g_converter_output_stream_new (mem, G_CONVERTER (decompressor));
|
|
|
|
if (!g_output_stream_write_all (out, g_bytes_get_data (bytes, NULL), g_bytes_get_size (bytes),
|
|
NULL, NULL, error))
|
|
return NULL;
|
|
|
|
if (!g_output_stream_close (out, NULL, error))
|
|
return NULL;
|
|
|
|
return g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (mem));
|
|
}
|
|
|
|
GBytes *
|
|
flatpak_read_stream (GInputStream *in,
|
|
gboolean null_terminate,
|
|
GError **error)
|
|
{
|
|
g_autoptr(GOutputStream) mem_stream = NULL;
|
|
|
|
mem_stream = g_memory_output_stream_new_resizable ();
|
|
if (g_output_stream_splice (mem_stream, in,
|
|
0, NULL, error) < 0)
|
|
return NULL;
|
|
|
|
if (null_terminate)
|
|
{
|
|
if (!g_output_stream_write (G_OUTPUT_STREAM (mem_stream), "\0", 1, NULL, error))
|
|
return NULL;
|
|
}
|
|
|
|
if (!g_output_stream_close (G_OUTPUT_STREAM (mem_stream), NULL, error))
|
|
return NULL;
|
|
|
|
return g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (mem_stream));
|
|
}
|
|
|
|
gint
|
|
flatpak_strcmp0_ptr (gconstpointer a,
|
|
gconstpointer b)
|
|
{
|
|
return g_strcmp0 (*(char * const *) a, *(char * const *) b);
|
|
}
|
|
|
|
/* Sometimes this is /var/run which is a symlink, causing weird issues when we pass
|
|
* it as a path into the sandbox */
|
|
char *
|
|
flatpak_get_real_xdg_runtime_dir (void)
|
|
{
|
|
return realpath (g_get_user_runtime_dir (), NULL);
|
|
}
|
|
|
|
/* Compares if str has a specific path prefix. This differs
|
|
from a regular prefix in two ways. First of all there may
|
|
be multiple slashes separating the path elements, and
|
|
secondly, if a prefix is matched that has to be en entire
|
|
path element. For instance /a/prefix matches /a/prefix/foo/bar,
|
|
but not /a/prefixfoo/bar. */
|
|
gboolean
|
|
flatpak_has_path_prefix (const char *str,
|
|
const char *prefix)
|
|
{
|
|
while (TRUE)
|
|
{
|
|
/* Skip consecutive slashes to reach next path
|
|
element */
|
|
while (*str == '/')
|
|
str++;
|
|
while (*prefix == '/')
|
|
prefix++;
|
|
|
|
/* No more prefix path elements? Done! */
|
|
if (*prefix == 0)
|
|
return TRUE;
|
|
|
|
/* Compare path element */
|
|
while (*prefix != 0 && *prefix != '/')
|
|
{
|
|
if (*str != *prefix)
|
|
return FALSE;
|
|
str++;
|
|
prefix++;
|
|
}
|
|
|
|
/* Matched prefix path element,
|
|
must be entire str path element */
|
|
if (*str != '/' && *str != 0)
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/* Returns end of matching path prefix, or NULL if no match */
|
|
const char *
|
|
flatpak_path_match_prefix (const char *pattern,
|
|
const char *string)
|
|
{
|
|
char c, test;
|
|
const char *tmp;
|
|
|
|
while (*pattern == '/')
|
|
pattern++;
|
|
|
|
while (*string == '/')
|
|
string++;
|
|
|
|
while (TRUE)
|
|
{
|
|
switch (c = *pattern++)
|
|
{
|
|
case 0:
|
|
if (*string == '/' || *string == 0)
|
|
return string;
|
|
return NULL;
|
|
|
|
case '?':
|
|
if (*string == '/' || *string == 0)
|
|
return NULL;
|
|
string++;
|
|
break;
|
|
|
|
case '*':
|
|
c = *pattern;
|
|
|
|
while (c == '*')
|
|
c = *++pattern;
|
|
|
|
/* special case * at end */
|
|
if (c == 0)
|
|
{
|
|
tmp = strchr (string, '/');
|
|
if (tmp != NULL)
|
|
return tmp;
|
|
return string + strlen (string);
|
|
}
|
|
else if (c == '/')
|
|
{
|
|
string = strchr (string, '/');
|
|
if (string == NULL)
|
|
return NULL;
|
|
break;
|
|
}
|
|
|
|
while ((test = *string) != 0)
|
|
{
|
|
tmp = flatpak_path_match_prefix (pattern, string);
|
|
if (tmp != NULL)
|
|
return tmp;
|
|
if (test == '/')
|
|
break;
|
|
string++;
|
|
}
|
|
return NULL;
|
|
|
|
default:
|
|
if (c != *string)
|
|
return NULL;
|
|
string++;
|
|
break;
|
|
}
|
|
}
|
|
return NULL; /* Should not be reached */
|
|
}
|
|
|
|
static const char *
|
|
flatpak_get_kernel_arch (void)
|
|
{
|
|
static struct utsname buf;
|
|
static const char *arch = NULL;
|
|
char *m;
|
|
|
|
if (arch != NULL)
|
|
return arch;
|
|
|
|
if (uname (&buf))
|
|
{
|
|
arch = "unknown";
|
|
return arch;
|
|
}
|
|
|
|
/* By default, just pass on machine, good enough for most arches */
|
|
arch = buf.machine;
|
|
|
|
/* Override for some arches */
|
|
|
|
m = buf.machine;
|
|
/* i?86 */
|
|
if (strlen (m) == 4 && m[0] == 'i' && m[2] == '8' && m[3] == '6')
|
|
{
|
|
arch = "i386";
|
|
}
|
|
else if (g_str_has_prefix (m, "arm"))
|
|
{
|
|
if (g_str_has_suffix (m, "b"))
|
|
arch = "armeb";
|
|
else
|
|
arch = "arm";
|
|
}
|
|
else if (strcmp (m, "mips") == 0)
|
|
{
|
|
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
|
|
arch = "mipsel";
|
|
#endif
|
|
}
|
|
else if (strcmp (m, "mips64") == 0)
|
|
{
|
|
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
|
|
arch = "mips64el";
|
|
#endif
|
|
}
|
|
|
|
return arch;
|
|
}
|
|
|
|
/* This maps the kernel-reported uname to a single string representing
|
|
* the cpu family, in the sense that all members of this family would
|
|
* be able to understand and link to a binary file with such cpu
|
|
* opcodes. That doesn't necessarily mean that all members of the
|
|
* family can run all opcodes, for instance for modern 32bit intel we
|
|
* report "i386", even though they support instructions that the
|
|
* original i386 cpu cannot run. Still, such an executable would
|
|
* at least try to execute a 386, whereas an arm binary would not.
|
|
*/
|
|
const char *
|
|
flatpak_get_arch (void)
|
|
{
|
|
/* Avoid using uname on multiarch machines, because uname reports the kernels
|
|
* arch, and that may be different from userspace. If e.g. the kernel is 64bit and
|
|
* the userspace is 32bit we want to use 32bit by default. So, we take the current build
|
|
* arch as the default. */
|
|
#if defined(__i386__)
|
|
return "i386";
|
|
#elif defined(__x86_64__)
|
|
return "x86_64";
|
|
#elif defined(__aarch64__)
|
|
return "aarch64";
|
|
#elif defined(__arm__)
|
|
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
|
|
return "arm";
|
|
#else
|
|
return "armeb";
|
|
#endif
|
|
#else
|
|
return flatpak_get_kernel_arch ();
|
|
#endif
|
|
}
|
|
|
|
gboolean
|
|
flatpak_is_linux32_arch (const char *arch)
|
|
{
|
|
const char *kernel_arch = flatpak_get_kernel_arch ();
|
|
|
|
if (strcmp (kernel_arch, "x86_64") == 0 &&
|
|
strcmp (arch, "i386") == 0)
|
|
return TRUE;
|
|
|
|
if (strcmp (kernel_arch, "aarch64") == 0 &&
|
|
strcmp (arch, "arm") == 0)
|
|
return TRUE;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static struct
|
|
{
|
|
const char *kernel_arch;
|
|
const char *compat_arch;
|
|
} compat_arches[] = {
|
|
{ "x86_64", "i386" },
|
|
{ "aarch64", "arm" },
|
|
};
|
|
|
|
const char *
|
|
flatpak_get_compat_arch (const char *kernel_arch)
|
|
{
|
|
int i;
|
|
|
|
/* Also add all other arches that are compatible with the kernel arch */
|
|
for (i = 0; i < G_N_ELEMENTS (compat_arches); i++)
|
|
{
|
|
if (strcmp (compat_arches[i].kernel_arch, kernel_arch) == 0)
|
|
return compat_arches[i].compat_arch;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
const char *
|
|
flatpak_get_compat_arch_reverse (const char *compat_arch)
|
|
{
|
|
int i;
|
|
|
|
/* Also add all other arches that are compatible with the kernel arch */
|
|
for (i = 0; i < G_N_ELEMENTS (compat_arches); i++)
|
|
{
|
|
if (strcmp (compat_arches[i].compat_arch, compat_arch) == 0)
|
|
return compat_arches[i].kernel_arch;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
/* Get all compatible arches for this host in order of priority */
|
|
const char **
|
|
flatpak_get_arches (void)
|
|
{
|
|
static gsize arches = 0;
|
|
|
|
if (g_once_init_enter (&arches))
|
|
{
|
|
gsize new_arches = 0;
|
|
const char *main_arch = flatpak_get_arch ();
|
|
const char *kernel_arch = flatpak_get_kernel_arch ();
|
|
const char *compat_arch;
|
|
GPtrArray *array = g_ptr_array_new ();
|
|
|
|
/* This is the userspace arch, i.e. the one flatpak itself was
|
|
build for. It's always first. */
|
|
g_ptr_array_add (array, (char *) main_arch);
|
|
|
|
compat_arch = flatpak_get_compat_arch (kernel_arch);
|
|
if (g_strcmp0 (compat_arch, main_arch) != 0)
|
|
g_ptr_array_add (array, (char *) compat_arch);
|
|
|
|
g_ptr_array_add (array, NULL);
|
|
new_arches = (gsize) g_ptr_array_free (array, FALSE);
|
|
|
|
g_once_init_leave (&arches, new_arches);
|
|
}
|
|
|
|
return (const char **) arches;
|
|
}
|
|
|
|
static char *
|
|
get_os_release_value (const char *key,
|
|
const char *default_value)
|
|
{
|
|
const char *file = "/etc/os-release";
|
|
g_autofree char *contents = NULL;
|
|
g_autoptr(GKeyFile) keyfile = g_key_file_new ();
|
|
g_autoptr(GString) str = NULL;
|
|
g_autofree char *value = NULL;
|
|
g_autofree char *unquoted = NULL;
|
|
|
|
if (!g_file_test (file, G_FILE_TEST_EXISTS))
|
|
file = "/usr/lib/os-release";
|
|
|
|
if (!g_file_get_contents (file, &contents, NULL, NULL))
|
|
return g_strdup (default_value);
|
|
|
|
str = g_string_new (contents);
|
|
g_string_prepend (str, "[os-release]\n");
|
|
|
|
if (!g_key_file_load_from_data (keyfile, str->str, -1, G_KEY_FILE_NONE, NULL))
|
|
return g_strdup (default_value);
|
|
|
|
value = flatpak_keyfile_get_string_non_empty (keyfile, "os-release", key);
|
|
unquoted = value ? g_shell_unquote (value, NULL) : NULL;
|
|
|
|
if (!unquoted)
|
|
return g_strdup (default_value);
|
|
|
|
return g_steal_pointer (&unquoted);
|
|
}
|
|
|
|
char *
|
|
flatpak_get_os_release_id (void)
|
|
{
|
|
return get_os_release_value ("ID", "linux");
|
|
}
|
|
|
|
char *
|
|
flatpak_get_os_release_version_id (void)
|
|
{
|
|
return get_os_release_value ("VERSION_ID", "unknown");
|
|
}
|
|
|
|
const char **
|
|
flatpak_get_gl_drivers (void)
|
|
{
|
|
static gsize drivers = 0;
|
|
|
|
if (g_once_init_enter (&drivers))
|
|
{
|
|
gsize new_drivers;
|
|
char **new_drivers_c = 0;
|
|
const char *env = g_getenv ("FLATPAK_GL_DRIVERS");
|
|
if (env != NULL && *env != 0)
|
|
new_drivers_c = g_strsplit (env, ":", -1);
|
|
else
|
|
{
|
|
g_autofree char *nvidia_version = NULL;
|
|
char *dot;
|
|
GPtrArray *array = g_ptr_array_new ();
|
|
|
|
if (g_file_get_contents ("/sys/module/nvidia/version",
|
|
&nvidia_version, NULL, NULL))
|
|
{
|
|
g_strstrip (nvidia_version);
|
|
/* Convert dots to dashes */
|
|
while ((dot = strchr (nvidia_version, '.')) != NULL)
|
|
*dot = '-';
|
|
g_ptr_array_add (array, g_strconcat ("nvidia-", nvidia_version, NULL));
|
|
}
|
|
|
|
g_ptr_array_add (array, (char *) "default");
|
|
g_ptr_array_add (array, (char *) "host");
|
|
|
|
g_ptr_array_add (array, NULL);
|
|
new_drivers_c = (char **) g_ptr_array_free (array, FALSE);
|
|
}
|
|
|
|
new_drivers = (gsize) new_drivers_c;
|
|
g_once_init_leave (&drivers, new_drivers);
|
|
}
|
|
|
|
return (const char **) drivers;
|
|
}
|
|
|
|
static gboolean
|
|
flatpak_get_have_intel_gpu (void)
|
|
{
|
|
static int have_intel = -1;
|
|
|
|
if (have_intel == -1)
|
|
have_intel = g_file_test ("/sys/module/i915", G_FILE_TEST_EXISTS) || g_file_test ("/sys/module/xe", G_FILE_TEST_EXISTS);
|
|
|
|
return have_intel;
|
|
}
|
|
|
|
static GHashTable *
|
|
load_kernel_module_list (void)
|
|
{
|
|
GHashTable *modules = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
|
|
g_autofree char *modules_data = NULL;
|
|
g_autoptr(GError) error = NULL;
|
|
char *start, *end;
|
|
|
|
if (!g_file_get_contents ("/proc/modules", &modules_data, NULL, &error))
|
|
{
|
|
g_info ("Failed to read /proc/modules: %s", error->message);
|
|
return modules;
|
|
}
|
|
|
|
/* /proc/modules is a table of modules.
|
|
* Columns are split by spaces and rows by newlines.
|
|
* The first column is the name. */
|
|
start = modules_data;
|
|
while (TRUE)
|
|
{
|
|
end = strchr (start, ' ');
|
|
if (end == NULL)
|
|
break;
|
|
|
|
g_hash_table_add (modules, g_strndup (start, (end - start)));
|
|
|
|
start = strchr (end, '\n');
|
|
if (start == NULL)
|
|
break;
|
|
|
|
start++;
|
|
}
|
|
|
|
return modules;
|
|
}
|
|
|
|
static gboolean
|
|
flatpak_get_have_kernel_module (const char *module_name)
|
|
{
|
|
static GHashTable *kernel_modules = NULL;
|
|
|
|
if (g_once_init_enter (&kernel_modules))
|
|
g_once_init_leave (&kernel_modules, load_kernel_module_list ());
|
|
|
|
return g_hash_table_contains (kernel_modules, module_name);
|
|
}
|
|
|
|
static const char *
|
|
flatpak_get_gtk_theme (void)
|
|
{
|
|
static char *gtk_theme;
|
|
|
|
if (g_once_init_enter (>k_theme))
|
|
{
|
|
/* The schema may not be installed so check first */
|
|
GSettingsSchemaSource *source = g_settings_schema_source_get_default ();
|
|
g_autoptr(GSettingsSchema) schema = NULL;
|
|
|
|
if (source == NULL)
|
|
g_once_init_leave (>k_theme, g_strdup (""));
|
|
else
|
|
{
|
|
schema = g_settings_schema_source_lookup (source,
|
|
"org.gnome.desktop.interface", TRUE);
|
|
|
|
if (schema == NULL)
|
|
g_once_init_leave (>k_theme, g_strdup (""));
|
|
else
|
|
{
|
|
/* GSettings is used to store the theme if you use Wayland or GNOME.
|
|
* TODO: Check XSettings Net/ThemeName for other desktops.
|
|
* We don't care about any other method (like settings.ini) because they
|
|
* aren't passed through the sandbox anyway. */
|
|
g_autoptr(GSettings) settings = g_settings_new ("org.gnome.desktop.interface");
|
|
g_once_init_leave (>k_theme, g_settings_get_string (settings, "gtk-theme"));
|
|
}
|
|
}
|
|
}
|
|
|
|
return (const char *) gtk_theme;
|
|
}
|
|
|
|
const char *
|
|
flatpak_get_bwrap (void)
|
|
{
|
|
const char *e = g_getenv ("FLATPAK_BWRAP");
|
|
|
|
if (e != NULL)
|
|
return e;
|
|
return HELPER;
|
|
}
|
|
|
|
gboolean
|
|
flatpak_bwrap_is_unprivileged (void)
|
|
{
|
|
g_autofree char *path = g_find_program_in_path (flatpak_get_bwrap ());
|
|
struct stat st;
|
|
|
|
/* Various features are supported only if bwrap exists and is not setuid */
|
|
return
|
|
path != NULL &&
|
|
stat (path, &st) == 0 &&
|
|
(st.st_mode & S_ISUID) == 0;
|
|
}
|
|
|
|
static char *
|
|
line_get_word (char **line)
|
|
{
|
|
char *word = NULL;
|
|
|
|
while (g_ascii_isspace (**line))
|
|
(*line)++;
|
|
|
|
if (**line == 0)
|
|
return NULL;
|
|
|
|
word = *line;
|
|
|
|
while (**line && !g_ascii_isspace (**line))
|
|
(*line)++;
|
|
|
|
if (**line)
|
|
{
|
|
**line = 0;
|
|
(*line)++;
|
|
}
|
|
|
|
return word;
|
|
}
|
|
|
|
char *
|
|
flatpak_filter_glob_to_regexp (const char *glob,
|
|
gboolean runtime_only,
|
|
GError **error)
|
|
{
|
|
g_autoptr(GString) regexp = g_string_new ("");
|
|
int parts = 1;
|
|
gboolean empty_part;
|
|
|
|
if (g_str_has_prefix (glob, "app/"))
|
|
{
|
|
if (runtime_only)
|
|
{
|
|
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Glob can't match apps"));
|
|
return NULL;
|
|
}
|
|
else
|
|
{
|
|
glob += strlen ("app/");
|
|
g_string_append (regexp, "app/");
|
|
}
|
|
}
|
|
else if (g_str_has_prefix (glob, "runtime/"))
|
|
{
|
|
glob += strlen ("runtime/");
|
|
g_string_append (regexp, "runtime/");
|
|
}
|
|
else
|
|
{
|
|
if (runtime_only)
|
|
g_string_append (regexp, "runtime/");
|
|
else
|
|
g_string_append (regexp, "(app|runtime)/");
|
|
}
|
|
|
|
/* We really need an id part, the rest is optional */
|
|
if (*glob == 0)
|
|
{
|
|
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Empty glob"));
|
|
return NULL;
|
|
}
|
|
|
|
empty_part = TRUE;
|
|
while (*glob != 0)
|
|
{
|
|
char c = *glob;
|
|
glob++;
|
|
|
|
if (c == '/')
|
|
{
|
|
if (empty_part)
|
|
g_string_append (regexp, "[.\\-_a-zA-Z0-9]*");
|
|
empty_part = TRUE;
|
|
parts++;
|
|
g_string_append (regexp, "/");
|
|
if (parts > 3)
|
|
{
|
|
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Too many segments in glob"));
|
|
return NULL;
|
|
}
|
|
}
|
|
else if (c == '*')
|
|
{
|
|
empty_part = FALSE;
|
|
g_string_append (regexp, "[.\\-_a-zA-Z0-9]*");
|
|
}
|
|
else if (c == '.')
|
|
{
|
|
empty_part = FALSE;
|
|
g_string_append (regexp, "\\.");
|
|
}
|
|
else if (g_ascii_isalnum (c) || c == '-' || c == '_')
|
|
{
|
|
empty_part = FALSE;
|
|
g_string_append_c (regexp, c);
|
|
}
|
|
else
|
|
{
|
|
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Invalid glob character '%c'"), c);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
while (parts < 3)
|
|
{
|
|
parts++;
|
|
g_string_append (regexp, "/[.\\-_a-zA-Z0-9]*");
|
|
}
|
|
|
|
return g_string_free (g_steal_pointer (®exp), FALSE);
|
|
}
|
|
|
|
gboolean
|
|
flatpak_parse_filters (const char *data,
|
|
GRegex **allow_refs_out,
|
|
GRegex **deny_refs_out,
|
|
GError **error)
|
|
{
|
|
g_auto(GStrv) lines = NULL;
|
|
int i;
|
|
g_autoptr(GString) allow_regexp = g_string_new ("^(");
|
|
g_autoptr(GString) deny_regexp = g_string_new ("^(");
|
|
gboolean has_allow = FALSE;
|
|
gboolean has_deny = FALSE;
|
|
g_autoptr(GRegex) allow_refs = NULL;
|
|
g_autoptr(GRegex) deny_refs = NULL;
|
|
|
|
lines = g_strsplit (data, "\n", -1);
|
|
for (i = 0; lines[i] != NULL; i++)
|
|
{
|
|
char *line = lines[i];
|
|
char *comment, *command;
|
|
|
|
/* Ignore shell-style comments */
|
|
comment = strchr (line, '#');
|
|
if (comment != NULL)
|
|
*comment = 0;
|
|
|
|
command = line_get_word (&line);
|
|
/* Ignore empty lines */
|
|
if (command == NULL)
|
|
continue;
|
|
|
|
if (strcmp (command, "allow") == 0 || strcmp (command, "deny") == 0)
|
|
{
|
|
char *glob, *next;
|
|
g_autofree char *ref_regexp = NULL;
|
|
GString *command_regexp;
|
|
gboolean *has_type = NULL;
|
|
|
|
glob = line_get_word (&line);
|
|
if (glob == NULL)
|
|
return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Missing glob on line %d"), i + 1);
|
|
|
|
next = line_get_word (&line);
|
|
if (next != NULL)
|
|
return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Trailing text on line %d"), i + 1);
|
|
|
|
ref_regexp = flatpak_filter_glob_to_regexp (glob, FALSE, error);
|
|
if (ref_regexp == NULL)
|
|
return glnx_prefix_error (error, _("on line %d"), i + 1);
|
|
|
|
if (strcmp (command, "allow") == 0)
|
|
{
|
|
command_regexp = allow_regexp;
|
|
has_type = &has_allow;
|
|
}
|
|
else
|
|
{
|
|
command_regexp = deny_regexp;
|
|
has_type = &has_deny;
|
|
}
|
|
|
|
if (*has_type)
|
|
g_string_append (command_regexp, "|");
|
|
else
|
|
*has_type = TRUE;
|
|
|
|
g_string_append (command_regexp, ref_regexp);
|
|
}
|
|
else
|
|
{
|
|
return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Unexpected word '%s' on line %d"), command, i + 1);
|
|
}
|
|
}
|
|
|
|
g_string_append (allow_regexp, ")$");
|
|
g_string_append (deny_regexp, ")$");
|
|
|
|
if (allow_regexp)
|
|
{
|
|
allow_refs = g_regex_new (allow_regexp->str, G_REGEX_DOLLAR_ENDONLY|G_REGEX_RAW|G_REGEX_OPTIMIZE, G_REGEX_MATCH_ANCHORED, error);
|
|
if (allow_refs == NULL)
|
|
return FALSE;
|
|
}
|
|
|
|
if (deny_regexp)
|
|
{
|
|
deny_refs = g_regex_new (deny_regexp->str, G_REGEX_DOLLAR_ENDONLY|G_REGEX_RAW|G_REGEX_OPTIMIZE, G_REGEX_MATCH_ANCHORED, error);
|
|
if (deny_refs == NULL)
|
|
return FALSE;
|
|
}
|
|
|
|
*allow_refs_out = g_steal_pointer (&allow_refs);
|
|
*deny_refs_out = g_steal_pointer (&deny_refs);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
flatpak_filters_allow_ref (GRegex *allow_refs,
|
|
GRegex *deny_refs,
|
|
const char *ref)
|
|
{
|
|
if (deny_refs == NULL)
|
|
return TRUE; /* All refs are allowed by default */
|
|
|
|
if (!g_regex_match (deny_refs, ref, G_REGEX_MATCH_ANCHORED, NULL))
|
|
return TRUE; /* Not denied */
|
|
|
|
if (allow_refs && g_regex_match (allow_refs, ref, G_REGEX_MATCH_ANCHORED, NULL))
|
|
return TRUE; /* Explicitly allowed */
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
remove_dangling_symlinks (int parent_fd,
|
|
const char *name,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
gboolean ret = FALSE;
|
|
struct dirent *dent;
|
|
g_auto(GLnxDirFdIterator) iter = { 0 };
|
|
|
|
if (!glnx_dirfd_iterator_init_at (parent_fd, name, FALSE, &iter, error))
|
|
goto out;
|
|
|
|
while (TRUE)
|
|
{
|
|
if (!glnx_dirfd_iterator_next_dent_ensure_dtype (&iter, &dent, cancellable, error))
|
|
goto out;
|
|
|
|
if (dent == NULL)
|
|
break;
|
|
|
|
if (dent->d_type == DT_DIR)
|
|
{
|
|
if (!remove_dangling_symlinks (iter.fd, dent->d_name, cancellable, error))
|
|
goto out;
|
|
}
|
|
else if (dent->d_type == DT_LNK)
|
|
{
|
|
struct stat stbuf;
|
|
if (fstatat (iter.fd, dent->d_name, &stbuf, 0) != 0 && errno == ENOENT)
|
|
{
|
|
if (unlinkat (iter.fd, dent->d_name, 0) != 0)
|
|
{
|
|
glnx_set_error_from_errno (error);
|
|
goto out;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ret = TRUE;
|
|
out:
|
|
|
|
return ret;
|
|
}
|
|
|
|
gboolean
|
|
flatpak_remove_dangling_symlinks (GFile *dir,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
gboolean ret = FALSE;
|
|
|
|
/* The fd is closed by this call */
|
|
if (!remove_dangling_symlinks (AT_FDCWD, flatpak_file_get_path_cached (dir),
|
|
cancellable, error))
|
|
goto out;
|
|
|
|
ret = TRUE;
|
|
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
/* This atomically replaces a symlink with a new value, removing the
|
|
* existing symlink target, if it exstis and is different from
|
|
* @target. This is atomic in the sense that we're guaranteed to
|
|
* remove any existing symlink target (once), independent of how many
|
|
* processes do the same operation in parallele. However, it is still
|
|
* possible that we remove the old and then fail to create the new
|
|
* symlink for some reason, ending up with neither the old or the new
|
|
* target. That is fine if the reason for the symlink is keeping a
|
|
* cache though.
|
|
*/
|
|
gboolean
|
|
flatpak_switch_symlink_and_remove (const char *symlink_path,
|
|
const char *target,
|
|
GError **error)
|
|
{
|
|
g_autofree char *symlink_dir = g_path_get_dirname (symlink_path);
|
|
int try;
|
|
|
|
for (try = 0; try < 100; try++)
|
|
{
|
|
g_autofree char *tmp_path = NULL;
|
|
int fd;
|
|
|
|
/* Try to atomically create the symlink */
|
|
if (TEMP_FAILURE_RETRY (symlink (target, symlink_path)) == 0)
|
|
return TRUE;
|
|
|
|
if (errno != EEXIST)
|
|
{
|
|
/* Unexpected failure, bail */
|
|
glnx_set_error_from_errno (error);
|
|
return FALSE;
|
|
}
|
|
|
|
/* The symlink existed, move it to a temporary name atomically, and remove target
|
|
if that succeeded. */
|
|
tmp_path = g_build_filename (symlink_dir, ".switched-symlink-XXXXXX", NULL);
|
|
|
|
fd = g_mkstemp_full (tmp_path, O_RDWR, 0644);
|
|
if (fd == -1)
|
|
{
|
|
glnx_set_error_from_errno (error);
|
|
return FALSE;
|
|
}
|
|
close (fd);
|
|
|
|
if (TEMP_FAILURE_RETRY (rename (symlink_path, tmp_path)) == 0)
|
|
{
|
|
/* The move succeeded, now we can remove the old target */
|
|
g_autofree char *old_target = flatpak_readlink (tmp_path, error);
|
|
if (old_target == NULL)
|
|
return FALSE;
|
|
if (strcmp (old_target, target) != 0) /* Don't remove old file if its the same as the new one */
|
|
{
|
|
g_autofree char *old_target_path = g_build_filename (symlink_dir, old_target, NULL);
|
|
unlink (old_target_path);
|
|
}
|
|
}
|
|
else if (errno != ENOENT)
|
|
{
|
|
glnx_set_error_from_errno (error);
|
|
unlink (tmp_path);
|
|
return -1;
|
|
}
|
|
unlink (tmp_path);
|
|
|
|
/* An old target was removed, try again */
|
|
}
|
|
|
|
return flatpak_fail (error, "flatpak_switch_symlink_and_remove looped too many times");
|
|
}
|
|
|
|
gboolean
|
|
flatpak_argument_needs_quoting (const char *arg)
|
|
{
|
|
if (*arg == '\0')
|
|
return TRUE;
|
|
|
|
while (*arg != 0)
|
|
{
|
|
char c = *arg;
|
|
if (!g_ascii_isalnum (c) &&
|
|
!(c == '-' || c == '/' || c == '~' ||
|
|
c == ':' || c == '.' || c == '_' ||
|
|
c == '=' || c == '@'))
|
|
return TRUE;
|
|
arg++;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
char *
|
|
flatpak_quote_argv (const char *argv[],
|
|
gssize len)
|
|
{
|
|
GString *res = g_string_new ("");
|
|
int i;
|
|
|
|
if (len == -1)
|
|
len = g_strv_length ((char **) argv);
|
|
|
|
for (i = 0; i < len; i++)
|
|
{
|
|
if (i != 0)
|
|
g_string_append_c (res, ' ');
|
|
|
|
if (flatpak_argument_needs_quoting (argv[i]))
|
|
{
|
|
g_autofree char *quoted = g_shell_quote (argv[i]);
|
|
g_string_append (res, quoted);
|
|
}
|
|
else
|
|
g_string_append (res, argv[i]);
|
|
}
|
|
|
|
return g_string_free (res, FALSE);
|
|
}
|
|
|
|
/* This is useful, because it handles escaped characters in uris, and ? arguments at the end of the uri */
|
|
gboolean
|
|
flatpak_file_arg_has_suffix (const char *arg, const char *suffix)
|
|
{
|
|
g_autoptr(GFile) file = g_file_new_for_commandline_arg (arg);
|
|
g_autofree char *basename = g_file_get_basename (file);
|
|
|
|
return g_str_has_suffix (basename, suffix);
|
|
}
|
|
|
|
GFile *
|
|
flatpak_build_file_va (GFile *base,
|
|
va_list args)
|
|
{
|
|
g_autoptr(GFile) res = g_object_ref (base);
|
|
const gchar *arg;
|
|
|
|
while ((arg = va_arg (args, const gchar *)))
|
|
{
|
|
g_autoptr(GFile) child = g_file_resolve_relative_path (res, arg);
|
|
g_set_object (&res, child);
|
|
}
|
|
|
|
return g_steal_pointer (&res);
|
|
}
|
|
|
|
GFile *
|
|
flatpak_build_file (GFile *base, ...)
|
|
{
|
|
GFile *res;
|
|
va_list args;
|
|
|
|
va_start (args, base);
|
|
res = flatpak_build_file_va (base, args);
|
|
va_end (args);
|
|
|
|
return res;
|
|
}
|
|
|
|
const char *
|
|
flatpak_file_get_path_cached (GFile *file)
|
|
{
|
|
const char *path;
|
|
static GQuark _file_path_quark = 0;
|
|
|
|
if (G_UNLIKELY (_file_path_quark == 0))
|
|
_file_path_quark = g_quark_from_static_string ("flatpak-file-path");
|
|
|
|
do
|
|
{
|
|
path = g_object_get_qdata ((GObject *) file, _file_path_quark);
|
|
if (path == NULL)
|
|
{
|
|
g_autofree char *new_path = NULL;
|
|
new_path = g_file_get_path (file);
|
|
if (new_path == NULL)
|
|
return NULL;
|
|
|
|
if (g_object_replace_qdata ((GObject *) file, _file_path_quark,
|
|
NULL, new_path, g_free, NULL))
|
|
path = g_steal_pointer (&new_path);
|
|
}
|
|
}
|
|
while (path == NULL);
|
|
|
|
return path;
|
|
}
|
|
|
|
gboolean
|
|
flatpak_openat_noatime (int dfd,
|
|
const char *name,
|
|
int *ret_fd,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
int fd;
|
|
int flags = O_RDONLY | O_CLOEXEC;
|
|
|
|
#ifdef O_NOATIME
|
|
do
|
|
fd = openat (dfd, name, flags | O_NOATIME, 0);
|
|
while (G_UNLIKELY (fd == -1 && errno == EINTR));
|
|
/* Only the owner or superuser may use O_NOATIME; so we may get
|
|
* EPERM. EINVAL may happen if the kernel is really old...
|
|
*/
|
|
if (fd == -1 && (errno == EPERM || errno == EINVAL))
|
|
#endif
|
|
do
|
|
fd = openat (dfd, name, flags, 0);
|
|
while (G_UNLIKELY (fd == -1 && errno == EINTR));
|
|
|
|
if (fd == -1)
|
|
{
|
|
glnx_set_error_from_errno (error);
|
|
return FALSE;
|
|
}
|
|
else
|
|
{
|
|
*ret_fd = fd;
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
gboolean
|
|
flatpak_cp_a (GFile *src,
|
|
GFile *dest,
|
|
FlatpakCpFlags flags,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
gboolean ret = FALSE;
|
|
GFileEnumerator *enumerator = NULL;
|
|
GFileInfo *src_info = NULL;
|
|
GFile *dest_child = NULL;
|
|
int dest_dfd = -1;
|
|
gboolean merge = (flags & FLATPAK_CP_FLAGS_MERGE) != 0;
|
|
gboolean no_chown = (flags & FLATPAK_CP_FLAGS_NO_CHOWN) != 0;
|
|
gboolean move = (flags & FLATPAK_CP_FLAGS_MOVE) != 0;
|
|
g_autoptr(GFileInfo) child_info = NULL;
|
|
GError *temp_error = NULL;
|
|
int r;
|
|
|
|
enumerator = g_file_enumerate_children (src, "standard::type,standard::name,unix::uid,unix::gid,unix::mode",
|
|
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
|
|
cancellable, error);
|
|
if (!enumerator)
|
|
goto out;
|
|
|
|
src_info = g_file_query_info (src, "standard::name,unix::mode,unix::uid,unix::gid," \
|
|
"time::modified,time::modified-usec,time::access,time::access-usec",
|
|
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
|
|
cancellable, error);
|
|
if (!src_info)
|
|
goto out;
|
|
|
|
do
|
|
r = mkdir (flatpak_file_get_path_cached (dest), 0755);
|
|
while (G_UNLIKELY (r == -1 && errno == EINTR));
|
|
if (r == -1 &&
|
|
(!merge || errno != EEXIST))
|
|
{
|
|
glnx_set_error_from_errno (error);
|
|
goto out;
|
|
}
|
|
|
|
if (!glnx_opendirat (AT_FDCWD, flatpak_file_get_path_cached (dest), TRUE,
|
|
&dest_dfd, error))
|
|
goto out;
|
|
|
|
if (!no_chown)
|
|
{
|
|
do
|
|
r = fchown (dest_dfd,
|
|
g_file_info_get_attribute_uint32 (src_info, "unix::uid"),
|
|
g_file_info_get_attribute_uint32 (src_info, "unix::gid"));
|
|
while (G_UNLIKELY (r == -1 && errno == EINTR));
|
|
if (r == -1)
|
|
{
|
|
glnx_set_error_from_errno (error);
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
do
|
|
r = fchmod (dest_dfd, g_file_info_get_attribute_uint32 (src_info, "unix::mode"));
|
|
while (G_UNLIKELY (r == -1 && errno == EINTR));
|
|
|
|
if (dest_dfd != -1)
|
|
{
|
|
(void) close (dest_dfd);
|
|
dest_dfd = -1;
|
|
}
|
|
|
|
while ((child_info = g_file_enumerator_next_file (enumerator, cancellable, &temp_error)))
|
|
{
|
|
const char *name = g_file_info_get_name (child_info);
|
|
g_autoptr(GFile) src_child = g_file_get_child (src, name);
|
|
|
|
if (dest_child)
|
|
g_object_unref (dest_child);
|
|
dest_child = g_file_get_child (dest, name);
|
|
|
|
if (g_file_info_get_file_type (child_info) == G_FILE_TYPE_DIRECTORY)
|
|
{
|
|
if (!flatpak_cp_a (src_child, dest_child, flags,
|
|
cancellable, error))
|
|
goto out;
|
|
}
|
|
else
|
|
{
|
|
(void) unlink (flatpak_file_get_path_cached (dest_child));
|
|
GFileCopyFlags copyflags = G_FILE_COPY_OVERWRITE | G_FILE_COPY_NOFOLLOW_SYMLINKS;
|
|
if (!no_chown)
|
|
copyflags |= G_FILE_COPY_ALL_METADATA;
|
|
if (move)
|
|
{
|
|
if (!g_file_move (src_child, dest_child, copyflags,
|
|
cancellable, NULL, NULL, error))
|
|
goto out;
|
|
}
|
|
else
|
|
{
|
|
if (!g_file_copy (src_child, dest_child, copyflags,
|
|
cancellable, NULL, NULL, error))
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
g_clear_object (&child_info);
|
|
}
|
|
|
|
if (temp_error != NULL)
|
|
{
|
|
g_propagate_error (error, temp_error);
|
|
goto out;
|
|
}
|
|
|
|
if (move &&
|
|
!g_file_delete (src, NULL, error))
|
|
goto out;
|
|
|
|
ret = TRUE;
|
|
out:
|
|
if (dest_dfd != -1)
|
|
(void) close (dest_dfd);
|
|
g_clear_object (&src_info);
|
|
g_clear_object (&enumerator);
|
|
g_clear_object (&dest_child);
|
|
return ret;
|
|
}
|
|
|
|
static gboolean
|
|
_flatpak_canonicalize_permissions (int parent_dfd,
|
|
const char *rel_path,
|
|
gboolean toplevel,
|
|
int uid,
|
|
int gid,
|
|
GError **error)
|
|
{
|
|
struct stat stbuf;
|
|
gboolean res = TRUE;
|
|
|
|
/* Note, in order to not leave non-canonical things around in case
|
|
* of error, this continues after errors, but returns the first
|
|
* error. */
|
|
|
|
if (TEMP_FAILURE_RETRY (fstatat (parent_dfd, rel_path, &stbuf, AT_SYMLINK_NOFOLLOW)) != 0)
|
|
{
|
|
glnx_set_error_from_errno (error);
|
|
return FALSE;
|
|
}
|
|
|
|
if ((uid != -1 && uid != stbuf.st_uid) || (gid != -1 && gid != stbuf.st_gid))
|
|
{
|
|
if (TEMP_FAILURE_RETRY (fchownat (parent_dfd, rel_path, uid, gid, AT_SYMLINK_NOFOLLOW)) != 0)
|
|
{
|
|
glnx_set_error_from_errno (error);
|
|
return FALSE;
|
|
}
|
|
|
|
/* Re-read st_mode for new owner */
|
|
if (TEMP_FAILURE_RETRY (fstatat (parent_dfd, rel_path, &stbuf, AT_SYMLINK_NOFOLLOW)) != 0)
|
|
{
|
|
glnx_set_error_from_errno (error);
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
if (S_ISDIR (stbuf.st_mode))
|
|
{
|
|
g_auto(GLnxDirFdIterator) dfd_iter = { 0, };
|
|
|
|
/* For the toplevel we set to 0700 so we can modify it, but not
|
|
expose any non-canonical files to any other user, then we set
|
|
it to 0755 afterwards. */
|
|
if (fchmodat (parent_dfd, rel_path, toplevel ? 0700 : 0755, 0) != 0)
|
|
{
|
|
glnx_set_error_from_errno (error);
|
|
error = NULL;
|
|
res = FALSE;
|
|
}
|
|
|
|
if (glnx_dirfd_iterator_init_at (parent_dfd, rel_path, FALSE, &dfd_iter, NULL))
|
|
{
|
|
while (TRUE)
|
|
{
|
|
struct dirent *dent;
|
|
|
|
if (!glnx_dirfd_iterator_next_dent (&dfd_iter, &dent, NULL, NULL) || dent == NULL)
|
|
break;
|
|
|
|
if (!_flatpak_canonicalize_permissions (dfd_iter.fd, dent->d_name, FALSE, uid, gid, error))
|
|
{
|
|
error = NULL;
|
|
res = FALSE;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (toplevel &&
|
|
fchmodat (parent_dfd, rel_path, 0755, 0) != 0)
|
|
{
|
|
glnx_set_error_from_errno (error);
|
|
error = NULL;
|
|
res = FALSE;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
else if (S_ISREG (stbuf.st_mode))
|
|
{
|
|
mode_t mode;
|
|
|
|
/* If use can execute, make executable by all */
|
|
if (stbuf.st_mode & S_IXUSR)
|
|
mode = 0755;
|
|
else /* otherwise executable by none */
|
|
mode = 0644;
|
|
|
|
if (fchmodat (parent_dfd, rel_path, mode, 0) != 0)
|
|
{
|
|
glnx_set_error_from_errno (error);
|
|
res = FALSE;
|
|
}
|
|
}
|
|
else if (S_ISLNK (stbuf.st_mode))
|
|
{
|
|
/* symlinks have no permissions */
|
|
}
|
|
else
|
|
{
|
|
/* some weird non-canonical type, lets delete it */
|
|
if (unlinkat (parent_dfd, rel_path, 0) != 0)
|
|
{
|
|
glnx_set_error_from_errno (error);
|
|
res = FALSE;
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
/* Canonicalizes files to the same permissions as bare-user-only checkouts */
|
|
gboolean
|
|
flatpak_canonicalize_permissions (int parent_dfd,
|
|
const char *rel_path,
|
|
int uid,
|
|
int gid,
|
|
GError **error)
|
|
{
|
|
return _flatpak_canonicalize_permissions (parent_dfd, rel_path, TRUE, uid, gid, error);
|
|
}
|
|
|
|
/* Make a directory, and its parent. Don't error if it already exists.
|
|
* If you want a failure mode with EEXIST, use g_file_make_directory_with_parents. */
|
|
gboolean
|
|
flatpak_mkdir_p (GFile *dir,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
return glnx_shutil_mkdir_p_at (AT_FDCWD,
|
|
flatpak_file_get_path_cached (dir),
|
|
0777,
|
|
cancellable,
|
|
error);
|
|
}
|
|
|
|
gboolean
|
|
flatpak_rm_rf (GFile *dir,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
return glnx_shutil_rm_rf_at (AT_FDCWD,
|
|
flatpak_file_get_path_cached (dir),
|
|
cancellable, error);
|
|
}
|
|
|
|
gboolean
|
|
flatpak_file_rename (GFile *from,
|
|
GFile *to,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
if (g_cancellable_set_error_if_cancelled (cancellable, error))
|
|
return FALSE;
|
|
|
|
if (rename (flatpak_file_get_path_cached (from),
|
|
flatpak_file_get_path_cached (to)) < 0)
|
|
{
|
|
glnx_set_error_from_errno (error);
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/* If memfd_create() is available, generate a sealed memfd with contents of
|
|
* @str. Otherwise use an O_TMPFILE @tmpf in anonymous mode, write @str to
|
|
* @tmpf, and lseek() back to the start. See also similar uses in e.g.
|
|
* rpm-ostree for running dracut.
|
|
*/
|
|
gboolean
|
|
flatpak_buffer_to_sealed_memfd_or_tmpfile (GLnxTmpfile *tmpf,
|
|
const char *name,
|
|
const char *str,
|
|
size_t len,
|
|
GError **error)
|
|
{
|
|
if (len == -1)
|
|
len = strlen (str);
|
|
glnx_autofd int memfd = memfd_create (name, MFD_CLOEXEC | MFD_ALLOW_SEALING);
|
|
int fd; /* Unowned */
|
|
if (memfd != -1)
|
|
{
|
|
fd = memfd;
|
|
}
|
|
else
|
|
{
|
|
/* We use an anonymous fd (i.e. O_EXCL) since we don't want
|
|
* the target container to potentially be able to re-link it.
|
|
*/
|
|
if (!G_IN_SET (errno, ENOSYS, EOPNOTSUPP))
|
|
return glnx_throw_errno_prefix (error, "memfd_create");
|
|
if (!glnx_open_anonymous_tmpfile (O_RDWR | O_CLOEXEC, tmpf, error))
|
|
return FALSE;
|
|
fd = tmpf->fd;
|
|
}
|
|
if (ftruncate (fd, len) < 0)
|
|
return glnx_throw_errno_prefix (error, "ftruncate");
|
|
if (glnx_loop_write (fd, str, len) < 0)
|
|
return glnx_throw_errno_prefix (error, "write");
|
|
if (lseek (fd, 0, SEEK_SET) < 0)
|
|
return glnx_throw_errno_prefix (error, "lseek");
|
|
if (memfd != -1)
|
|
{
|
|
/* Valgrind doesn't currently handle G_ADD_SEALS, so lets not seal when debugging... */
|
|
if ((!RUNNING_ON_VALGRIND) &&
|
|
fcntl (memfd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE | F_SEAL_SEAL) < 0)
|
|
return glnx_throw_errno_prefix (error, "fcntl(F_ADD_SEALS)");
|
|
/* The other values can stay default */
|
|
tmpf->fd = g_steal_fd (&memfd);
|
|
tmpf->initialized = TRUE;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
flatpak_open_in_tmpdir_at (int tmpdir_fd,
|
|
int mode,
|
|
char *tmpl,
|
|
GOutputStream **out_stream,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
const int max_attempts = 128;
|
|
int i;
|
|
int fd;
|
|
|
|
/* 128 attempts seems reasonable... */
|
|
for (i = 0; i < max_attempts; i++)
|
|
{
|
|
glnx_gen_temp_name (tmpl);
|
|
|
|
do
|
|
fd = openat (tmpdir_fd, tmpl, O_WRONLY | O_CREAT | O_EXCL, mode);
|
|
while (fd == -1 && errno == EINTR);
|
|
if (fd < 0 && errno != EEXIST)
|
|
{
|
|
glnx_set_error_from_errno (error);
|
|
return FALSE;
|
|
}
|
|
else if (fd != -1)
|
|
break;
|
|
}
|
|
if (i == max_attempts)
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
|
"Exhausted attempts to open temporary file");
|
|
return FALSE;
|
|
}
|
|
|
|
if (out_stream)
|
|
*out_stream = g_unix_output_stream_new (fd, TRUE);
|
|
else
|
|
(void) close (fd);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
flatpak_bytes_save (GFile *dest,
|
|
GBytes *bytes,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
g_autoptr(GOutputStream) out = NULL;
|
|
|
|
out = (GOutputStream *) g_file_replace (dest, NULL, FALSE,
|
|
G_FILE_CREATE_REPLACE_DESTINATION,
|
|
cancellable, error);
|
|
if (out == NULL)
|
|
return FALSE;
|
|
|
|
if (!g_output_stream_write_all (out,
|
|
g_bytes_get_data (bytes, NULL),
|
|
g_bytes_get_size (bytes),
|
|
NULL,
|
|
cancellable,
|
|
error))
|
|
return FALSE;
|
|
|
|
if (!g_output_stream_close (out, cancellable, error))
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
flatpak_variant_save (GFile *dest,
|
|
GVariant *variant,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
g_autoptr(GOutputStream) out = NULL;
|
|
gsize bytes_written;
|
|
|
|
out = (GOutputStream *) g_file_replace (dest, NULL, FALSE,
|
|
G_FILE_CREATE_REPLACE_DESTINATION,
|
|
cancellable, error);
|
|
if (out == NULL)
|
|
return FALSE;
|
|
|
|
if (!g_output_stream_write_all (out,
|
|
g_variant_get_data (variant),
|
|
g_variant_get_size (variant),
|
|
&bytes_written,
|
|
cancellable,
|
|
error))
|
|
return FALSE;
|
|
|
|
if (!g_output_stream_close (out, cancellable, error))
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
char *
|
|
flatpak_keyfile_get_string_non_empty (GKeyFile *keyfile,
|
|
const char *group,
|
|
const char *key)
|
|
{
|
|
g_autofree char *value = NULL;
|
|
|
|
value = g_key_file_get_string (keyfile, group, key, NULL);
|
|
if (value != NULL && *value == '\0')
|
|
g_clear_pointer (&value, g_free);
|
|
|
|
return g_steal_pointer (&value);
|
|
}
|
|
|
|
gboolean
|
|
flatpak_extension_matches_reason (const char *extension_id,
|
|
const char *reasons,
|
|
gboolean default_value)
|
|
{
|
|
const char *extension_basename;
|
|
g_auto(GStrv) reason_list = NULL;
|
|
size_t i;
|
|
|
|
if (reasons == NULL || *reasons == 0)
|
|
return default_value;
|
|
|
|
extension_basename = strrchr (extension_id, '.');
|
|
if (extension_basename == NULL)
|
|
return FALSE;
|
|
extension_basename += 1;
|
|
|
|
reason_list = g_strsplit (reasons, ";", -1);
|
|
|
|
for (i = 0; reason_list[i]; ++i)
|
|
{
|
|
const char *reason = reason_list[i];
|
|
|
|
if (strcmp (reason, "active-gl-driver") == 0)
|
|
{
|
|
/* handled below */
|
|
const char **gl_drivers = flatpak_get_gl_drivers ();
|
|
size_t j;
|
|
|
|
for (j = 0; gl_drivers[j]; j++)
|
|
{
|
|
if (strcmp (gl_drivers[j], extension_basename) == 0)
|
|
return TRUE;
|
|
}
|
|
}
|
|
else if (strcmp (reason, "active-gtk-theme") == 0)
|
|
{
|
|
const char *gtk_theme = flatpak_get_gtk_theme ();
|
|
if (strcmp (gtk_theme, extension_basename) == 0)
|
|
return TRUE;
|
|
}
|
|
else if (strcmp (reason, "have-intel-gpu") == 0)
|
|
{
|
|
/* Used for Intel VAAPI driver extension */
|
|
if (flatpak_get_have_intel_gpu ())
|
|
return TRUE;
|
|
}
|
|
else if (g_str_has_prefix (reason, "have-kernel-module-"))
|
|
{
|
|
const char *module_name = reason + strlen ("have-kernel-module-");
|
|
|
|
if (flatpak_get_have_kernel_module (module_name))
|
|
return TRUE;
|
|
}
|
|
else if (g_str_has_prefix (reason, "on-xdg-desktop-"))
|
|
{
|
|
const char *desktop_name = reason + strlen ("on-xdg-desktop-");
|
|
const char *current_desktop_var = g_getenv ("XDG_CURRENT_DESKTOP");
|
|
g_auto(GStrv) current_desktop_names = NULL;
|
|
size_t j;
|
|
|
|
if (!current_desktop_var)
|
|
continue;
|
|
|
|
current_desktop_names = g_strsplit (current_desktop_var, ":", -1);
|
|
|
|
for (j = 0; current_desktop_names[j]; ++j)
|
|
{
|
|
if (g_ascii_strcasecmp (desktop_name, current_desktop_names[j]) == 0)
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
void
|
|
flatpak_parse_extension_with_tag (const char *extension,
|
|
char **name,
|
|
char **tag)
|
|
{
|
|
const char *tag_chr = strchr (extension, '@');
|
|
|
|
if (tag_chr)
|
|
{
|
|
if (name != NULL)
|
|
*name = g_strndup (extension, tag_chr - extension);
|
|
|
|
/* Everything after the @ */
|
|
if (tag != NULL)
|
|
*tag = g_strdup (tag_chr + 1);
|
|
|
|
return;
|
|
}
|
|
|
|
if (name != NULL)
|
|
*name = g_strdup (extension);
|
|
|
|
if (tag != NULL)
|
|
*tag = NULL;
|
|
}
|
|
|
|
/* This allocates and locks a subdir of the tmp dir, using an existing
|
|
* one with the same prefix if it is not in use already. */
|
|
gboolean
|
|
flatpak_allocate_tmpdir (int tmpdir_dfd,
|
|
const char *tmpdir_relpath,
|
|
const char *tmpdir_prefix,
|
|
char **tmpdir_name_out,
|
|
int *tmpdir_fd_out,
|
|
GLnxLockFile *file_lock_out,
|
|
gboolean *reusing_dir_out,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
gboolean reusing_dir = FALSE;
|
|
g_autofree char *tmpdir_name = NULL;
|
|
glnx_autofd int tmpdir_fd = -1;
|
|
g_auto(GLnxDirFdIterator) dfd_iter = { 0, };
|
|
|
|
/* Look for existing tmpdir (with same prefix) to reuse */
|
|
if (!glnx_dirfd_iterator_init_at (tmpdir_dfd, tmpdir_relpath ? tmpdir_relpath : ".", FALSE, &dfd_iter, error))
|
|
return FALSE;
|
|
|
|
while (tmpdir_name == NULL)
|
|
{
|
|
struct dirent *dent;
|
|
glnx_autofd int existing_tmpdir_fd = -1;
|
|
g_autoptr(GError) local_error = NULL;
|
|
g_autofree char *lock_name = NULL;
|
|
|
|
if (!glnx_dirfd_iterator_next_dent (&dfd_iter, &dent, cancellable, error))
|
|
return FALSE;
|
|
|
|
if (dent == NULL)
|
|
break;
|
|
|
|
if (!g_str_has_prefix (dent->d_name, tmpdir_prefix))
|
|
continue;
|
|
|
|
/* Quickly skip non-dirs, if unknown we ignore ENOTDIR when opening instead */
|
|
if (dent->d_type != DT_UNKNOWN &&
|
|
dent->d_type != DT_DIR)
|
|
continue;
|
|
|
|
if (!glnx_opendirat (dfd_iter.fd, dent->d_name, FALSE,
|
|
&existing_tmpdir_fd, &local_error))
|
|
{
|
|
if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY))
|
|
{
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
g_propagate_error (error, g_steal_pointer (&local_error));
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
lock_name = g_strconcat (dent->d_name, "-lock", NULL);
|
|
|
|
/* We put the lock outside the dir, so we can hold the lock
|
|
* until the directory is fully removed */
|
|
if (!glnx_make_lock_file (dfd_iter.fd, lock_name, LOCK_EX | LOCK_NB,
|
|
file_lock_out, &local_error))
|
|
{
|
|
if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK))
|
|
{
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
g_propagate_error (error, g_steal_pointer (&local_error));
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
/* Touch the reused directory so that we don't accidentally
|
|
* remove it due to being old when cleaning up the tmpdir
|
|
*/
|
|
(void) futimens (existing_tmpdir_fd, NULL);
|
|
|
|
/* We found an existing tmpdir which we managed to lock */
|
|
tmpdir_name = g_strdup (dent->d_name);
|
|
tmpdir_fd = g_steal_fd (&existing_tmpdir_fd);
|
|
reusing_dir = TRUE;
|
|
}
|
|
|
|
while (tmpdir_name == NULL)
|
|
{
|
|
g_autofree char *tmpdir_name_template = g_strconcat (tmpdir_prefix, "XXXXXX", NULL);
|
|
g_autoptr(GError) local_error = NULL;
|
|
g_autofree char *lock_name = NULL;
|
|
g_auto(GLnxTmpDir) new_tmpdir = { 0, };
|
|
/* No existing tmpdir found, create a new */
|
|
|
|
if (!glnx_mkdtempat (dfd_iter.fd, tmpdir_name_template, 0777,
|
|
&new_tmpdir, error))
|
|
return FALSE;
|
|
|
|
lock_name = g_strconcat (new_tmpdir.path, "-lock", NULL);
|
|
|
|
/* Note, at this point we can race with another process that picks up this
|
|
* new directory. If that happens we need to retry, making a new directory. */
|
|
if (!glnx_make_lock_file (dfd_iter.fd, lock_name, LOCK_EX | LOCK_NB,
|
|
file_lock_out, &local_error))
|
|
{
|
|
if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK))
|
|
{
|
|
glnx_tmpdir_unset (&new_tmpdir); /* Don't delete */
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
g_propagate_error (error, g_steal_pointer (&local_error));
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
tmpdir_name = g_strdup (new_tmpdir.path);
|
|
tmpdir_fd = dup (new_tmpdir.fd);
|
|
glnx_tmpdir_unset (&new_tmpdir); /* Don't delete */
|
|
}
|
|
|
|
if (tmpdir_name_out)
|
|
*tmpdir_name_out = g_steal_pointer (&tmpdir_name);
|
|
|
|
if (tmpdir_fd_out)
|
|
*tmpdir_fd_out = g_steal_fd (&tmpdir_fd);
|
|
|
|
if (reusing_dir_out)
|
|
*reusing_dir_out = reusing_dir;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
/* Carefully opens a file from a base directory and subpath,
|
|
* making sure that its not a symlink.
|
|
*/
|
|
int
|
|
flatpak_open_file_at (int dfd,
|
|
const char *subpath,
|
|
struct stat *st_buf,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
glnx_autofd int fd = -1;
|
|
struct stat tmp_st_buf;
|
|
|
|
do
|
|
fd = openat (dfd, subpath, O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_NOCTTY);
|
|
while (G_UNLIKELY (fd == -1 && errno == EINTR));
|
|
if (fd == -1)
|
|
{
|
|
glnx_set_error_from_errno (error);
|
|
return -1;
|
|
}
|
|
|
|
if (st_buf == NULL)
|
|
st_buf = &tmp_st_buf;
|
|
|
|
if (fstat (fd, st_buf) != 0)
|
|
{
|
|
glnx_set_error_from_errno (error);
|
|
return -1;
|
|
}
|
|
|
|
if (!S_ISREG (st_buf->st_mode))
|
|
{
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
|
|
"Non-regular file not allowed at %s", subpath);
|
|
return -1;
|
|
}
|
|
|
|
return g_steal_fd (&fd);
|
|
}
|
|
|
|
/* Carefully gets the content of a file from a base directory and
|
|
* subpath, making sure that its not a symlink.
|
|
*/
|
|
GBytes *
|
|
flatpak_load_file_at (int dfd,
|
|
const char *subpath,
|
|
GCancellable *cancellable,
|
|
GError **error)
|
|
{
|
|
glnx_autofd int fd = -1;
|
|
GBytes *bytes;
|
|
|
|
fd = flatpak_open_file_at (dfd, subpath, NULL, cancellable, error);
|
|
if (fd == -1)
|
|
return NULL;
|
|
|
|
bytes = glnx_fd_readall_bytes (fd, cancellable, error);
|
|
if (bytes == NULL)
|
|
return NULL;
|
|
|
|
return bytes;
|
|
}
|
|
|
|
static gint
|
|
string_length_compare_func (gconstpointer a,
|
|
gconstpointer b)
|
|
{
|
|
return strlen (*(char * const *) a) - strlen (*(char * const *) b);
|
|
}
|
|
|
|
/* Sort a string array by decreasing length */
|
|
char **
|
|
flatpak_strv_sort_by_length (const char * const *strv)
|
|
{
|
|
GPtrArray *array;
|
|
int i;
|
|
|
|
if (strv == NULL)
|
|
return NULL;
|
|
|
|
/* Combine both */
|
|
array = g_ptr_array_new ();
|
|
|
|
for (i = 0; strv[i] != NULL; i++)
|
|
g_ptr_array_add (array, g_strdup (strv[i]));
|
|
|
|
g_ptr_array_sort (array, string_length_compare_func);
|
|
|
|
g_ptr_array_add (array, NULL);
|
|
return (char **) g_ptr_array_free (array, FALSE);
|
|
}
|
|
|
|
char **
|
|
flatpak_strv_merge (char **strv1,
|
|
char **strv2)
|
|
{
|
|
GPtrArray *array;
|
|
int i;
|
|
|
|
/* Maybe either (or both) is unspecified */
|
|
if (strv1 == NULL)
|
|
return g_strdupv (strv2);
|
|
if (strv2 == NULL)
|
|
return g_strdupv (strv1);
|
|
|
|
/* Combine both */
|
|
array = g_ptr_array_new ();
|
|
|
|
for (i = 0; strv1[i] != NULL; i++)
|
|
{
|
|
if (!flatpak_g_ptr_array_contains_string (array, strv1[i]))
|
|
g_ptr_array_add (array, g_strdup (strv1[i]));
|
|
}
|
|
|
|
for (i = 0; strv2[i] != NULL; i++)
|
|
{
|
|
if (!flatpak_g_ptr_array_contains_string (array, strv2[i]))
|
|
g_ptr_array_add (array, g_strdup (strv2[i]));
|
|
}
|
|
|
|
g_ptr_array_add (array, NULL);
|
|
return (char **) g_ptr_array_free (array, FALSE);
|
|
}
|
|
|
|
/* In this NULL means don't care about these paths, while
|
|
an empty array means match anything */
|
|
char **
|
|
flatpak_subpaths_merge (char **subpaths1,
|
|
char **subpaths2)
|
|
{
|
|
char **res;
|
|
|
|
if (subpaths1 != NULL && subpaths1[0] == NULL)
|
|
return g_strdupv (subpaths1);
|
|
if (subpaths2 != NULL && subpaths2[0] == NULL)
|
|
return g_strdupv (subpaths2);
|
|
|
|
res = flatpak_strv_merge (subpaths1, subpaths2);
|
|
if (res)
|
|
qsort (res, g_strv_length (res), sizeof (const char *), flatpak_strcmp0_ptr);
|
|
|
|
return res;
|
|
}
|
|
|
|
gboolean
|
|
flatpak_g_ptr_array_contains_string (GPtrArray *array, const char *str)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < array->len; i++)
|
|
{
|
|
if (strcmp (g_ptr_array_index (array, i), str) == 0)
|
|
return TRUE;
|
|
}
|
|
return FALSE;
|
|
}
|
|
|
|
gboolean
|
|
flatpak_check_required_version (const char *ref,
|
|
GKeyFile *metakey,
|
|
GError **error)
|
|
{
|
|
g_auto(GStrv) required_versions = NULL;
|
|
const char *group;
|
|
int max_required_major = 0, max_required_minor = 0;
|
|
const char *max_required_version = "0.0";
|
|
int i;
|
|
|
|
if (g_str_has_prefix (ref, "app/"))
|
|
group = "Application";
|
|
else
|
|
group = "Runtime";
|
|
|
|
/* We handle handle multiple version requirements here. Each requirement must
|
|
* be in the form major.minor.micro, and if the flatpak version matches the
|
|
* major.minor part, t must be equal or later in the micro. If the major.minor part
|
|
* doesn't exactly match any of the specified requirements it must be larger
|
|
* than the maximum specified requirement.
|
|
*
|
|
* For example, specifying
|
|
* required-flatpak=1.6.2;1.4.2;1.0.2;
|
|
* would allow flatpak versions:
|
|
* 1.7.0, 1.6.2, 1.6.3, 1.4.2, 1.4.3, 1.0.2, 1.0.3
|
|
* but not:
|
|
* 1.6.1, 1.4.1 or 1.2.100.
|
|
*
|
|
* The goal here is to be able to specify a version (like 1.6.2 above) where a feature
|
|
* was introduced, but also allow backports of said feature to earlier version series.
|
|
*
|
|
* Earlier versions that only support specifying one version will only look at the first
|
|
* element in the list, so put the largest version first.
|
|
*/
|
|
required_versions = g_key_file_get_string_list (metakey, group, "required-flatpak", NULL, NULL);
|
|
if (required_versions == 0 || required_versions[0] == NULL)
|
|
return TRUE;
|
|
|
|
for (i = 0; required_versions[i] != NULL; i++)
|
|
{
|
|
int required_major, required_minor, required_micro;
|
|
const char *required_version = required_versions[i];
|
|
|
|
if (sscanf (required_version, "%d.%d.%d", &required_major, &required_minor, &required_micro) != 3)
|
|
return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA,
|
|
_("Invalid require-flatpak argument %s"), required_version);
|
|
else
|
|
{
|
|
/* If flatpak is in the same major.minor series as the requirement, do a micro check */
|
|
if (required_major == PACKAGE_MAJOR_VERSION && required_minor == PACKAGE_MINOR_VERSION)
|
|
{
|
|
if (required_micro <= PACKAGE_MICRO_VERSION)
|
|
return TRUE;
|
|
else
|
|
return flatpak_fail_error (error, FLATPAK_ERROR_NEED_NEW_FLATPAK,
|
|
_("%s needs a later flatpak version (%s)"),
|
|
ref, required_version);
|
|
}
|
|
|
|
/* Otherwise, keep track of the largest major.minor that is required */
|
|
if ((required_major > max_required_major) ||
|
|
(required_major == max_required_major &&
|
|
required_minor > max_required_minor))
|
|
{
|
|
max_required_major = required_major;
|
|
max_required_minor = required_minor;
|
|
max_required_version = required_version;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (max_required_major > PACKAGE_MAJOR_VERSION ||
|
|
(max_required_major == PACKAGE_MAJOR_VERSION && max_required_minor > PACKAGE_MINOR_VERSION))
|
|
return flatpak_fail_error (error, FLATPAK_ERROR_NEED_NEW_FLATPAK,
|
|
_("%s needs a later flatpak version (%s)"),
|
|
ref, max_required_version);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static int
|
|
dist (const char *s, int ls, const char *t, int lt, int i, int j, int *d)
|
|
{
|
|
int x, y;
|
|
|
|
if (d[i * (lt + 1) + j] >= 0)
|
|
return d[i * (lt + 1) + j];
|
|
|
|
if (i == ls)
|
|
x = lt - j;
|
|
else if (j == lt)
|
|
x = ls - i;
|
|
else if (s[i] == t[j])
|
|
x = dist (s, ls, t, lt, i + 1, j + 1, d);
|
|
else
|
|
{
|
|
x = dist (s, ls, t, lt, i + 1, j + 1, d);
|
|
y = dist (s, ls, t, lt, i, j + 1, d);
|
|
if (y < x)
|
|
x = y;
|
|
y = dist (s, ls, t, lt, i + 1, j, d);
|
|
if (y < x)
|
|
x = y;
|
|
x++;
|
|
}
|
|
|
|
d[i * (lt + 1) + j] = x;
|
|
|
|
return x;
|
|
}
|
|
|
|
int
|
|
flatpak_levenshtein_distance (const char *s,
|
|
gssize ls,
|
|
const char *t,
|
|
gssize lt)
|
|
{
|
|
int i, j;
|
|
int *d;
|
|
|
|
if (ls < 0)
|
|
ls = strlen (s);
|
|
|
|
if (lt < 0)
|
|
lt = strlen (t);
|
|
|
|
d = alloca (sizeof (int) * (ls + 1) * (lt + 1));
|
|
|
|
for (i = 0; i <= ls; i++)
|
|
for (j = 0; j <= lt; j++)
|
|
d[i * (lt + 1) + j] = -1;
|
|
|
|
return dist (s, ls, t, lt, 0, 0, d);
|
|
}
|
|
|
|
/* Convert an app id to a dconf path in the obvious way.
|
|
*/
|
|
char *
|
|
flatpak_dconf_path_for_app_id (const char *app_id)
|
|
{
|
|
GString *s;
|
|
const char *p;
|
|
|
|
s = g_string_new ("");
|
|
|
|
g_string_append_c (s, '/');
|
|
for (p = app_id; *p; p++)
|
|
{
|
|
if (*p == '.')
|
|
g_string_append_c (s, '/');
|
|
else
|
|
g_string_append_c (s, *p);
|
|
}
|
|
g_string_append_c (s, '/');
|
|
|
|
return g_string_free (s, FALSE);
|
|
}
|
|
|
|
/* Check if two dconf paths are 'similar enough', which
|
|
* for now is defined as equal except case differences
|
|
* and -/_
|
|
*/
|
|
gboolean
|
|
flatpak_dconf_path_is_similar (const char *path1,
|
|
const char *path2)
|
|
{
|
|
int i1, i2;
|
|
int num_components = -1;
|
|
|
|
for (i1 = i2 = 0; path1[i1] != '\0'; i1++, i2++)
|
|
{
|
|
if (path2[i2] == '\0')
|
|
break;
|
|
|
|
if (isupper(path2[i2]) &&
|
|
(path1[i1] == '-' || path1[i1] == '_'))
|
|
{
|
|
i1++;
|
|
if (path1[i1] == '\0')
|
|
break;
|
|
}
|
|
|
|
if (isupper(path1[i1]) &&
|
|
(path2[i2] == '-' || path2[i2] == '_'))
|
|
{
|
|
i2++;
|
|
if (path2[i2] == '\0')
|
|
break;
|
|
}
|
|
|
|
if (tolower (path1[i1]) == tolower (path2[i2]))
|
|
{
|
|
if (path1[i1] == '/')
|
|
num_components++;
|
|
continue;
|
|
}
|
|
|
|
if ((path1[i1] == '-' || path1[i1] == '_') &&
|
|
(path2[i2] == '-' || path2[i2] == '_'))
|
|
continue;
|
|
|
|
break;
|
|
}
|
|
|
|
/* Skip over any versioning if we have at least a TLD and
|
|
* domain name, so 2 components */
|
|
/* We need at least TLD, and domain name, so 2 components */
|
|
if (num_components >= 2)
|
|
{
|
|
while (isdigit (path1[i1]))
|
|
i1++;
|
|
while (isdigit (path2[i2]))
|
|
i2++;
|
|
}
|
|
|
|
if (path1[i1] != path2[i2])
|
|
return FALSE;
|
|
|
|
/* Both strings finished? */
|
|
if (path1[i1] == '\0')
|
|
return TRUE;
|
|
|
|
/* Maybe a trailing slash in both strings */
|
|
if (path1[i1] == '/')
|
|
{
|
|
i1++;
|
|
i2++;
|
|
}
|
|
|
|
if (path1[i1] != path2[i2])
|
|
return FALSE;
|
|
|
|
return (path1[i1] == '\0');
|
|
}
|
|
|
|
GStrv
|
|
flatpak_parse_env_block (const char *data,
|
|
gsize length,
|
|
GError **error)
|
|
{
|
|
g_autoptr(GPtrArray) env_vars = g_ptr_array_new_with_free_func (g_free);
|
|
const char *p = data;
|
|
gsize remaining = length;
|
|
|
|
/* env_block might not be \0-terminated */
|
|
while (remaining > 0)
|
|
{
|
|
size_t len = strnlen (p, remaining);
|
|
const char *equals;
|
|
|
|
g_assert (len <= remaining);
|
|
|
|
equals = memchr (p, '=', len);
|
|
|
|
if (equals == NULL || equals == p)
|
|
return glnx_null_throw (error,
|
|
"Environment variable must be in the form VARIABLE=VALUE, not %.*s", (int) len, p);
|
|
|
|
g_ptr_array_add (env_vars,
|
|
g_strndup (p, len));
|
|
|
|
p += len;
|
|
remaining -= len;
|
|
|
|
if (remaining > 0)
|
|
{
|
|
g_assert (*p == '\0');
|
|
p += 1;
|
|
remaining -= 1;
|
|
}
|
|
}
|
|
|
|
g_ptr_array_add (env_vars, NULL);
|
|
|
|
return (GStrv) g_ptr_array_free (g_steal_pointer (&env_vars), FALSE);
|
|
}
|
|
|
|
/**
|
|
* flatpak_envp_cmp:
|
|
* @p1: a `const char * const *`
|
|
* @p2: a `const char * const *`
|
|
*
|
|
* Compare two environment variables, given as pointers to pointers
|
|
* to the actual `KEY=value` string.
|
|
*
|
|
* In particular this is suitable for sorting a #GStrv using `qsort`.
|
|
*
|
|
* Returns: negative, 0 or positive if `*p1` compares before, equal to
|
|
* or after `*p2`
|
|
*/
|
|
int
|
|
flatpak_envp_cmp (const void *p1,
|
|
const void *p2)
|
|
{
|
|
const char * const * s1 = p1;
|
|
const char * const * s2 = p2;
|
|
size_t l1 = strlen (*s1);
|
|
size_t l2 = strlen (*s2);
|
|
size_t min;
|
|
const char *tmp;
|
|
int ret;
|
|
|
|
tmp = strchr (*s1, '=');
|
|
|
|
if (tmp != NULL)
|
|
l1 = tmp - *s1;
|
|
|
|
tmp = strchr (*s2, '=');
|
|
|
|
if (tmp != NULL)
|
|
l2 = tmp - *s2;
|
|
|
|
min = MIN (l1, l2);
|
|
ret = strncmp (*s1, *s2, min);
|
|
|
|
/* If they differ before the first '=' (if any) in either s1 or s2,
|
|
* then they are certainly different */
|
|
if (ret != 0)
|
|
return ret;
|
|
|
|
ret = strcmp (*s1, *s2);
|
|
|
|
/* If they do not differ at all, then they are equal */
|
|
if (ret == 0)
|
|
return ret;
|
|
|
|
/* FOO < FOO=..., and FOO < FOOBAR */
|
|
if ((*s1)[min] == '\0')
|
|
return -1;
|
|
|
|
/* FOO=... > FOO, and FOOBAR > FOO */
|
|
if ((*s2)[min] == '\0')
|
|
return 1;
|
|
|
|
/* FOO= < FOOBAR */
|
|
if ((*s1)[min] == '=' && (*s2)[min] != '=')
|
|
return -1;
|
|
|
|
/* FOOBAR > FOO= */
|
|
if ((*s2)[min] == '=' && (*s1)[min] != '=')
|
|
return 1;
|
|
|
|
/* Fall back to plain string comparison */
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Return %TRUE if @s consists of one or more digits.
|
|
* This is the same as Python bytes.isdigit().
|
|
*/
|
|
gboolean
|
|
flatpak_str_is_integer (const char *s)
|
|
{
|
|
if (s == NULL || *s == '\0')
|
|
return FALSE;
|
|
|
|
for (; *s != '\0'; s++)
|
|
{
|
|
if (!g_ascii_isdigit (*s))
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
flatpak_uri_equal (const char *uri1,
|
|
const char *uri2)
|
|
{
|
|
g_autofree char *uri1_norm = NULL;
|
|
g_autofree char *uri2_norm = NULL;
|
|
gsize uri1_len = strlen (uri1);
|
|
gsize uri2_len = strlen (uri2);
|
|
|
|
/* URIs handled by libostree are equivalent with or without a trailing slash,
|
|
* but this isn't otherwise guaranteed to be the case.
|
|
*/
|
|
if (g_str_has_prefix (uri1, "oci+") || g_str_has_prefix (uri2, "oci+"))
|
|
return g_strcmp0 (uri1, uri2) == 0;
|
|
|
|
if (g_str_has_suffix (uri1, "/"))
|
|
uri1_norm = g_strndup (uri1, uri1_len - 1);
|
|
else
|
|
uri1_norm = g_strdup (uri1);
|
|
|
|
if (g_str_has_suffix (uri2, "/"))
|
|
uri2_norm = g_strndup (uri2, uri2_len - 1);
|
|
else
|
|
uri2_norm = g_strdup (uri2);
|
|
|
|
return g_strcmp0 (uri1_norm, uri2_norm) == 0;
|
|
}
|
|
|
|
static gboolean
|
|
is_char_safe (gunichar c)
|
|
{
|
|
return g_unichar_isgraph (c) || c == ' ';
|
|
}
|
|
|
|
static gboolean
|
|
should_hex_escape (gunichar c,
|
|
FlatpakEscapeFlags flags)
|
|
{
|
|
if ((flags & FLATPAK_ESCAPE_ALLOW_NEWLINES) && c == '\n')
|
|
return FALSE;
|
|
|
|
return !is_char_safe (c);
|
|
}
|
|
|
|
static void
|
|
append_hex_escaped_character (GString *result,
|
|
gunichar c)
|
|
{
|
|
if (c <= 0xFF)
|
|
g_string_append_printf (result, "\\x%02X", c);
|
|
else if (c <= 0xFFFF)
|
|
g_string_append_printf (result, "\\u%04X", c);
|
|
else
|
|
g_string_append_printf (result, "\\U%08X", c);
|
|
}
|
|
|
|
static char *
|
|
escape_character (gunichar c)
|
|
{
|
|
g_autoptr(GString) res = g_string_new ("");
|
|
append_hex_escaped_character (res, c);
|
|
return g_string_free (g_steal_pointer (&res), FALSE);
|
|
}
|
|
|
|
char *
|
|
flatpak_escape_string (const char *s,
|
|
FlatpakEscapeFlags flags)
|
|
{
|
|
g_autoptr(GString) res = g_string_new ("");
|
|
gboolean did_escape = FALSE;
|
|
|
|
while (*s)
|
|
{
|
|
gunichar c = g_utf8_get_char_validated (s, -1);
|
|
if (c == (gunichar)-2 || c == (gunichar)-1)
|
|
{
|
|
/* Need to convert to unsigned first, to avoid negative chars becoming
|
|
huge gunichars. */
|
|
append_hex_escaped_character (res, (unsigned char)*s++);
|
|
did_escape = TRUE;
|
|
continue;
|
|
}
|
|
else if (should_hex_escape (c, flags))
|
|
{
|
|
append_hex_escaped_character (res, c);
|
|
did_escape = TRUE;
|
|
}
|
|
else if (c == '\\' || (!(flags & FLATPAK_ESCAPE_DO_NOT_QUOTE) && c == '\''))
|
|
{
|
|
g_string_append_printf (res, "\\%c", (char) c);
|
|
did_escape = TRUE;
|
|
}
|
|
else
|
|
g_string_append_unichar (res, c);
|
|
|
|
s = g_utf8_find_next_char (s, NULL);
|
|
}
|
|
|
|
if (did_escape && !(flags & FLATPAK_ESCAPE_DO_NOT_QUOTE))
|
|
{
|
|
g_string_prepend_c (res, '\'');
|
|
g_string_append_c (res, '\'');
|
|
}
|
|
|
|
return g_string_free (g_steal_pointer (&res), FALSE);
|
|
}
|
|
|
|
gboolean
|
|
flatpak_validate_path_characters (const char *path,
|
|
GError **error)
|
|
{
|
|
while (*path)
|
|
{
|
|
gunichar c = g_utf8_get_char_validated (path, -1);
|
|
if (c == (gunichar)-1 || c == (gunichar)-2)
|
|
{
|
|
/* Need to convert to unsigned first, to avoid negative chars becoming
|
|
huge gunichars. */
|
|
g_autofree char *escaped_char = escape_character ((unsigned char)*path);
|
|
g_autofree char *escaped = flatpak_escape_string (path, FLATPAK_ESCAPE_DEFAULT);
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
|
|
"Non-UTF8 byte %s in path %s", escaped_char, escaped);
|
|
return FALSE;
|
|
}
|
|
else if (!is_char_safe (c))
|
|
{
|
|
g_autofree char *escaped_char = escape_character (c);
|
|
g_autofree char *escaped = flatpak_escape_string (path, FLATPAK_ESCAPE_DEFAULT);
|
|
g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
|
|
"Non-graphical character %s in path %s", escaped_char, escaped);
|
|
return FALSE;
|
|
}
|
|
|
|
path = g_utf8_find_next_char (path, NULL);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
running_under_sudo_root (void)
|
|
{
|
|
const char *sudo_command_env = g_getenv ("SUDO_COMMAND");
|
|
g_auto(GStrv) split_command = NULL;
|
|
|
|
if (!sudo_command_env)
|
|
return FALSE;
|
|
|
|
/* SUDO_COMMAND could be a value like `/usr/bin/flatpak run foo` */
|
|
split_command = g_strsplit (sudo_command_env, " ", 2);
|
|
/* Check if sudo was used to run as root instead of non-root users
|
|
* using -u or -g for example. */
|
|
if (g_str_has_suffix (split_command[0], "flatpak") && geteuid () == 0)
|
|
return TRUE;
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean is_debugging = FALSE;
|
|
|
|
void
|
|
flatpak_set_debugging (gboolean debugging)
|
|
{
|
|
is_debugging = debugging;
|
|
}
|
|
|
|
gboolean
|
|
flatpak_is_debugging (void)
|
|
{
|
|
#if GLIB_CHECK_VERSION (2, 68, 0)
|
|
if (!g_log_writer_default_would_drop (G_LOG_LEVEL_DEBUG, G_LOG_DOMAIN))
|
|
return TRUE;
|
|
#endif
|
|
|
|
return is_debugging;
|
|
}
|
|
|
|
#ifdef INCLUDE_INTERNAL_TESTS
|
|
static GList *flatpak_test_paths = NULL;
|
|
static GList *flatpak_test_fns = NULL;
|
|
|
|
void flatpak_add_test (const char *path, flatpak_test_fn fn)
|
|
{
|
|
flatpak_test_paths = g_list_prepend (flatpak_test_paths, (void *)path);
|
|
flatpak_test_fns = g_list_prepend (flatpak_test_fns, fn);
|
|
}
|
|
#endif
|
|
|
|
void flatpak_add_all_tests (void)
|
|
{
|
|
#ifdef INCLUDE_INTERNAL_TESTS
|
|
for (GList *l1 = flatpak_test_paths, *l2 = flatpak_test_fns; l1 != NULL; l1 = l1->next, l2 = l2->next) {
|
|
g_test_add_func (l1->data, l2->data);
|
|
}
|
|
#endif
|
|
}
|