/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s:
* Copyright © 2018 Red Hat, Inc
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library. If not, see .
*
* Authors:
* Alexander Larsson
*/
#include "config.h"
#include "flatpak-complete.h"
#include "flatpak-installation.h"
#include "flatpak-utils-private.h"
/* 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,
const char *file_type)
{
flatpak_completion_debug ("completing FILE");
g_print ("%s\n", file_type);
}
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;
const char *shell_cur_end;
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 : "";
rest = string + strlen (completion->cur);
shell_cur_end = shell_cur + strlen (shell_cur);
while (shell_cur_end > shell_cur &&
rest > string &&
shell_cur_end[-1] == rest[-1] &&
/* I'm not sure exactly what bash is doing here with =, but this seems to work... */
shell_cur_end[-1] != '=')
{
rest--;
shell_cur_end--;
}
flatpak_completion_debug ("completing word: '%s' (%s)", string, rest);
g_print ("%s\n", rest);
}
void
flatpak_complete_ref_id (FlatpakCompletion *completion,
GPtrArray *refs)
{
if (refs == NULL)
return;
for (int i = 0; i < refs->len; i++)
{
FlatpakDecomposed *ref = g_ptr_array_index (refs, i);
g_autofree char *id = flatpak_decomposed_dup_id (ref);
flatpak_complete_word (completion, "%s ", id);
}
}
void
flatpak_complete_ref_branch (FlatpakCompletion *completion,
GPtrArray *refs)
{
if (refs == NULL)
return;
for (int i = 0; i < refs->len; i++)
{
FlatpakDecomposed *ref = g_ptr_array_index (refs, i);
g_autofree char *branch = flatpak_decomposed_dup_branch (ref);
flatpak_complete_word (completion, "%s ", branch);
}
}
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_autoptr(GPtrArray) 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)
{
g_autoptr(FlatpakRemoteState) state = get_remote_state (dir, remote, TRUE, FALSE,
(element > 2) ? arch : only_arch, NULL,
NULL, &error);
if (state != NULL)
refs = flatpak_dir_find_remote_refs (dir, state,
(element > 1) ? id : NULL,
(element > 3) ? branch : NULL,
NULL, /* default branch */
(element > 2) ? arch : only_arch,
NULL, /* default arch */
matched_kinds,
FIND_MATCHING_REFS_FLAGS_NONE,
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,
FIND_MATCHING_REFS_FLAGS_NONE,
&error);
}
if (refs == NULL)
flatpak_completion_debug ("find refs error: %s", error->message);
for (i = 0; refs != NULL && i < refs->len; i++)
{
FlatpakDecomposed *ref = g_ptr_array_index (refs, i);
int j;
g_autoptr(GString) comp = NULL;
g_auto(GStrv) parts = g_strsplit (flatpak_decomposed_get_ref (ref), "/", 0);
if (!g_str_has_prefix (parts[element], cur_parts[element]))
continue;
if (flatpak_id_has_subref_suffix (parts[element], -1))
{
char *last_dot = strrchr (parts[element], '.');
if (last_dot == NULL)
continue; /* Shouldn't really happen */
/* Only complete to subrefs if fully matching real part.
* For example, only match org.foo.Bar.Sources for
* "org.foo.Bar", "org.foo.Bar." or "org.foo.Bar.S", but
* not for "org.foo" or other shorter prefixes.
*/
if (strncmp (parts[element], cur_parts[element], last_dot - parts[element] - 1) != 0)
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;
if (completion->cur == NULL || completion->cur[0] != '-')
return;
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, "__FLATPAK_FILE");
}
else if (strcmp (e->long_name, "installation") == 0)
{
g_autoptr(GPtrArray) installations = NULL;
installations = flatpak_get_system_installations (NULL, NULL);
for (i = 0; i < installations->len; i++)
{
FlatpakInstallation *inst = g_ptr_array_index (installations, i);
flatpak_complete_word (completion, "%s%s ", prefix, flatpak_installation_get_id (inst));
}
}
else if (strcmp (e->long_name, "columns") == 0)
{
/* columns are treated separately */
}
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 void
flatpak_complete_column (FlatpakCompletion *completion,
char **used,
const char *column)
{
g_autoptr(GString) s = NULL;
s = g_string_new ("");
if (used[0] != NULL)
{
int i;
if (g_strv_contains ((const char * const *) used, column))
return;
const char *last = NULL;
last = used[g_strv_length (used) - 1];
if (!g_str_has_prefix (column, last))
return;
for (i = 0; used[i + 1]; i++)
{
g_string_append (s, used[i]);
g_string_append_c (s, ',');
}
}
g_string_append (s, column);
flatpak_completion_debug ("completing column: %s", s->str);
g_print ("%s\n", s->str);
}
void
flatpak_complete_columns (FlatpakCompletion *completion,
Column *columns)
{
int i;
const char *list = NULL;
g_auto(GStrv) used = NULL;
if (!g_str_has_prefix (completion->cur, "--columns="))
return;
list = completion->cur + strlen ("--columns=");
if (strcmp (list, "all") == 0 ||
strcmp (list, "help") == 0)
return;
used = g_strsplit (list, ",", 0);
flatpak_completion_debug ("complete columns, used: '%s'", list);
if (g_strv_length (used) <= 1)
{
flatpak_complete_column (completion, used, "all");
flatpak_complete_column (completion, used, "help");
}
for (i = 0; columns[i].name; i++)
flatpak_complete_column (completion, used, columns[i].name);
}
void
flatpak_complete_context (FlatpakCompletion *completion)
{
flatpak_complete_options (completion, flatpak_context_get_option_entries ());
}
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 (cursor > 0 && !is_word_separator (s[cursor - 1]))
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_memdup2 (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;
/* Ensure we're not going oob if we got weird arguments. */
_point = MIN (_point, strlen (arg_line));
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 ("%s", 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_free (completion->shell_cur);
g_strfreev (completion->original_argv);
g_free (completion);
}