PIKApp/app/plug-in/pikainterpreterdb.c

880 lines
21 KiB
C

/* PIKA - Photo and Image Kooker Application
* a rebranding of The GNU Image Manipulation Program (created with heckimp)
* A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
*
* Original copyright, applying to most contents (license remains unchanged):
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
*
* pikainterpreterdb.c
* (C) 2005 Manish Singh <yosh@gimp.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* The binfmt_misc bits are derived from linux/fs/binfmt_misc.c
* Copyright (C) 1997 Richard Günther
*/
/*
* The sh-bang code is derived from linux/fs/binfmt_script.c
* Copyright (C) 1996 Martin von Löwis
* original #!-checking implemented by tytso.
*/
#include "config.h"
#include <stdlib.h>
#include <string.h>
#include <gio/gio.h>
#include "libpikabase/pikabase.h"
#include "plug-in-types.h"
#include "pikainterpreterdb.h"
#include "pika-intl.h"
#define BUFSIZE 4096
typedef struct _PikaInterpreterMagic PikaInterpreterMagic;
struct _PikaInterpreterMagic
{
gulong offset;
gchar *magic;
gchar *mask;
guint size;
gchar *program;
};
static void pika_interpreter_db_finalize (GObject *object);
static void pika_interpreter_db_load_interp_file (PikaInterpreterDB *db,
GFile *file);
static void pika_interpreter_db_add_program (PikaInterpreterDB *db,
GFile *file,
gchar *buffer);
static void pika_interpreter_db_add_binfmt_misc (PikaInterpreterDB *db,
GFile *file,
gchar *buffer);
static gboolean pika_interpreter_db_add_extension (GFile *file,
PikaInterpreterDB *db,
gchar **tokens);
static gboolean pika_interpreter_db_add_magic (PikaInterpreterDB *db,
gchar **tokens);
static void pika_interpreter_db_clear_magics (PikaInterpreterDB *db);
static void pika_interpreter_db_resolve_programs (PikaInterpreterDB *db);
G_DEFINE_TYPE (PikaInterpreterDB, pika_interpreter_db, G_TYPE_OBJECT)
#define parent_class pika_interpreter_db_parent_class
static void
pika_interpreter_db_class_init (PikaInterpreterDBClass *class)
{
GObjectClass *object_class = G_OBJECT_CLASS (class);
object_class->finalize = pika_interpreter_db_finalize;
}
static void
pika_interpreter_db_init (PikaInterpreterDB *db)
{
}
static void
pika_interpreter_db_finalize (GObject *object)
{
PikaInterpreterDB *db = PIKA_INTERPRETER_DB (object);
pika_interpreter_db_clear (db);
G_OBJECT_CLASS (parent_class)->finalize (object);
}
PikaInterpreterDB *
pika_interpreter_db_new (gboolean verbose)
{
PikaInterpreterDB *db = g_object_new (PIKA_TYPE_INTERPRETER_DB, NULL);
db->verbose = verbose;
return db;
}
void
pika_interpreter_db_load (PikaInterpreterDB *db,
GList *path)
{
GList *list;
g_return_if_fail (PIKA_IS_INTERPRETER_DB (db));
pika_interpreter_db_clear (db);
db->programs = g_hash_table_new_full (g_str_hash, g_str_equal,
g_free, g_free);
db->extensions = g_hash_table_new_full (g_str_hash, g_str_equal,
g_free, g_free);
db->magic_names = g_hash_table_new_full (g_str_hash, g_str_equal,
g_free, NULL);
db->extension_names = g_hash_table_new_full (g_str_hash, g_str_equal,
g_free, NULL);
for (list = path; list; list = g_list_next (list))
{
GFile *dir = list->data;
GFileEnumerator *enumerator;
enumerator =
g_file_enumerate_children (dir,
G_FILE_ATTRIBUTE_STANDARD_NAME ","
G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","
G_FILE_ATTRIBUTE_STANDARD_TYPE,
G_FILE_QUERY_INFO_NONE,
NULL, NULL);
if (enumerator)
{
GFileInfo *info;
while ((info = g_file_enumerator_next_file (enumerator,
NULL, NULL)))
{
if (! g_file_info_get_is_hidden (info) &&
g_file_info_get_file_type (info) == G_FILE_TYPE_REGULAR)
{
GFile *file = g_file_enumerator_get_child (enumerator, info);
pika_interpreter_db_load_interp_file (db, file);
g_object_unref (file);
}
g_object_unref (info);
}
g_object_unref (enumerator);
}
}
pika_interpreter_db_resolve_programs (db);
}
void
pika_interpreter_db_clear (PikaInterpreterDB *db)
{
g_return_if_fail (PIKA_IS_INTERPRETER_DB (db));
if (db->magic_names)
{
g_hash_table_destroy (db->magic_names);
db->magic_names = NULL;
}
if (db->extension_names)
{
g_hash_table_destroy (db->extension_names);
db->extension_names = NULL;
}
if (db->programs)
{
g_hash_table_destroy (db->programs);
db->programs = NULL;
}
if (db->extensions)
{
g_hash_table_destroy (db->extensions);
db->extensions = NULL;
}
pika_interpreter_db_clear_magics (db);
}
static void
pika_interpreter_db_load_interp_file (PikaInterpreterDB *db,
GFile *file)
{
GInputStream *input;
GDataInputStream *data_input;
gchar *buffer;
gsize buffer_len;
GError *error = NULL;
if (db->verbose)
g_print ("Parsing '%s'\n", pika_file_get_utf8_name (file));
input = G_INPUT_STREAM (g_file_read (file, NULL, &error));
if (! input)
{
g_message (_("Could not open '%s' for reading: %s"),
pika_file_get_utf8_name (file),
error->message);
g_clear_error (&error);
return;
}
data_input = g_data_input_stream_new (input);
g_object_unref (input);
while ((buffer = g_data_input_stream_read_line (data_input, &buffer_len,
NULL, &error)))
{
/* Skip comments */
if (buffer[0] == '#')
{
g_free (buffer);
continue;
}
if (g_ascii_isalnum (buffer[0]) || (buffer[0] == '/'))
{
pika_interpreter_db_add_program (db, file, buffer);
}
else if (! g_ascii_isspace (buffer[0]) && (buffer[0] != '\0'))
{
pika_interpreter_db_add_binfmt_misc (db, file, buffer);
}
g_free (buffer);
}
if (error)
{
g_message (_("Error reading '%s': %s"),
pika_file_get_utf8_name (file),
error->message);
g_clear_error (&error);
}
g_object_unref (data_input);
}
static void
pika_interpreter_db_add_program (PikaInterpreterDB *db,
GFile *file,
gchar *buffer)
{
gchar *name;
gchar *program;
gchar *p;
p = strchr (buffer, '=');
if (! p)
return;
*p = '\0';
name = buffer;
program = p + 1;
g_strchomp (program);
if (! g_file_test (program, G_FILE_TEST_IS_EXECUTABLE))
{
gchar *prog;
prog = g_find_program_in_path (program);
if (! prog || ! g_file_test (prog, G_FILE_TEST_IS_EXECUTABLE))
{
g_message (_("Bad interpreter referenced in interpreter file %s: %s"),
pika_file_get_utf8_name (file),
pika_filename_to_utf8 (program));
if (prog)
g_free (prog);
return;
}
program = prog;
}
else
program = g_strdup (program);
if (! g_hash_table_lookup (db->programs, name))
g_hash_table_insert (db->programs, g_strdup (name), program);
}
static void
pika_interpreter_db_add_binfmt_misc (PikaInterpreterDB *db,
GFile *file,
gchar *buffer)
{
gchar **tokens = NULL;
gchar *name, *type, *program;
gsize count;
gchar del[2];
count = strlen (buffer);
if ((count < 10) || (count > 255))
goto bail;
buffer = g_strndup (buffer, count + 9);
del[0] = *buffer;
del[1] = '\0';
memset (buffer + count, del[0], 8);
tokens = g_strsplit (buffer + 1, del, -1);
g_free (buffer);
name = tokens[0];
type = tokens[1];
program = tokens[5];
if ((name[0] == '\0') || (program[0] == '\0') ||
(type[0] == '\0') || (type[1] != '\0'))
goto bail;
switch (type[0])
{
case 'E':
if (! pika_interpreter_db_add_extension (file, db, tokens))
goto bail;
break;
case 'M':
if (! pika_interpreter_db_add_magic (db, tokens))
goto bail;
break;
default:
goto bail;
}
goto out;
bail:
g_message (_("Bad binary format string in interpreter file %s"),
pika_file_get_utf8_name (file));
out:
g_strfreev (tokens);
}
static gboolean
pika_interpreter_db_add_extension (GFile *file,
PikaInterpreterDB *db,
gchar **tokens)
{
const gchar *name = tokens[0];
const gchar *extension = tokens[3];
const gchar *program = tokens[5];
if (! g_hash_table_lookup (db->extension_names, name))
{
gchar *prog;
if (extension[0] == '\0' || extension[0] == '/')
return FALSE;
if (! g_file_test (program, G_FILE_TEST_IS_EXECUTABLE))
{
prog = g_find_program_in_path (program);
if (! prog || ! g_file_test (prog, G_FILE_TEST_IS_EXECUTABLE))
{
g_message (_("Bad interpreter referenced in interpreter file %s: %s"),
pika_file_get_utf8_name (file),
pika_filename_to_utf8 (program));
if (prog)
g_free (prog);
return FALSE;
}
}
else
prog = g_strdup (program);
g_hash_table_insert (db->extensions, g_strdup (extension), prog);
g_hash_table_insert (db->extension_names, g_strdup (name), prog);
}
return TRUE;
}
static gboolean
scanarg (const gchar *s)
{
gchar c;
while ((c = *s++) != '\0')
{
if (c == '\\' && *s == 'x')
{
s++;
if (! g_ascii_isxdigit (*s++))
return FALSE;
if (! g_ascii_isxdigit (*s++))
return FALSE;
}
}
return TRUE;
}
static guint
unquote (gchar *from)
{
gchar *s = from;
gchar *p = from;
gchar c;
while ((c = *s++) != '\0')
{
if (c == '\\' && *s == 'x')
{
s++;
*p = g_ascii_xdigit_value (*s++) << 4;
*p++ |= g_ascii_xdigit_value (*s++);
continue;
}
*p++ = c;
}
return p - from;
}
static gboolean
pika_interpreter_db_add_magic (PikaInterpreterDB *db,
gchar **tokens)
{
PikaInterpreterMagic *interp_magic;
gchar *name, *num, *magic, *mask, *program;
gulong offset;
guint size;
name = tokens[0];
num = tokens[2];
magic = tokens[3];
mask = tokens[4];
program = tokens[5];
if (! g_hash_table_lookup (db->magic_names, name))
{
if (num[0] != '\0')
{
offset = strtoul (num, &num, 10);
if (num[0] != '\0')
return FALSE;
if (offset > (BUFSIZE / 4))
return FALSE;
}
else
{
offset = 0;
}
if (! scanarg (magic))
return FALSE;
if (! scanarg (mask))
return FALSE;
size = unquote (magic);
if ((size + offset) > (BUFSIZE / 2))
return FALSE;
if (mask[0] == '\0')
mask = NULL;
else if (unquote (mask) != size)
return FALSE;
interp_magic = g_slice_new (PikaInterpreterMagic);
interp_magic->offset = offset;
interp_magic->magic = g_memdup2 (magic, size);
interp_magic->mask = g_memdup2 (mask, size);
interp_magic->size = size;
interp_magic->program = g_strdup (program);
db->magics = g_slist_append (db->magics, interp_magic);
g_hash_table_insert (db->magic_names, g_strdup (name), interp_magic);
}
return TRUE;
}
static void
pika_interpreter_db_clear_magics (PikaInterpreterDB *db)
{
PikaInterpreterMagic *magic;
GSList *list, *last;
list = db->magics;
db->magics = NULL;
while (list)
{
magic = list->data;
g_free (magic->magic);
g_free (magic->mask);
g_free (magic->program);
g_slice_free (PikaInterpreterMagic, magic);
last = list;
list = list->next;
g_slist_free_1 (last);
}
}
#ifdef INTERP_DEBUG
static void
print_kv (gpointer key,
gpointer value,
gpointer user_data)
{
g_print ("%s: %s\n", (gchar *) key, (gchar *) value);
}
static gchar *
quote (gchar *s,
guint size)
{
GString *d;
guint i;
if (s == NULL)
return "(null)";
d = g_string_sized_new (size * 4);
for (i = 0; i < size; i++)
g_string_append_printf (d, "\\x%02x", ((guint) s[i]) & 0xff);
return g_string_free (d, FALSE);
}
#endif
static gboolean
resolve_program (gpointer key,
gpointer value,
gpointer user_data)
{
PikaInterpreterDB *db = user_data;
gchar *program;
program = g_hash_table_lookup (db->programs, value);
if (program != NULL)
{
g_free (value);
value = g_strdup (program);
}
g_hash_table_insert (db->extensions, key, value);
return TRUE;
}
static void
pika_interpreter_db_resolve_programs (PikaInterpreterDB *db)
{
GSList *list;
GHashTable *extensions;
for (list = db->magics; list; list = list->next)
{
PikaInterpreterMagic *magic = list->data;
const gchar *program;
program = g_hash_table_lookup (db->programs, magic->program);
if (program != NULL)
{
g_free (magic->program);
magic->program = g_strdup (program);
}
}
extensions = db->extensions;
db->extensions = g_hash_table_new_full (g_str_hash, g_str_equal,
g_free, g_free);
g_hash_table_foreach_steal (extensions, resolve_program, db);
g_hash_table_destroy (extensions);
#ifdef INTERP_DEBUG
g_print ("Programs:\n");
g_hash_table_foreach (db->programs, print_kv, NULL);
g_print ("\nExtensions:\n");
g_hash_table_foreach (db->extensions, print_kv, NULL);
g_print ("\nMagics:\n");
list = db->magics;
while (list)
{
PikaInterpreterMagic *magic;
magic = list->data;
g_print ("program: %s, offset: %lu, magic: %s, mask: %s\n",
magic->program, magic->offset,
quote (magic->magic, magic->size),
quote (magic->mask, magic->size));
list = list->next;
}
g_print ("\n");
#endif
}
static gchar *
resolve_extension (PikaInterpreterDB *db,
const gchar *program_path)
{
gchar *filename;
gchar *p;
const gchar *program;
filename = g_path_get_basename (program_path);
p = strrchr (filename, '.');
if (! p)
{
g_free (filename);
return NULL;
}
program = g_hash_table_lookup (db->extensions, p + 1);
g_free (filename);
return g_strdup (program);
}
static gchar *
resolve_sh_bang (PikaInterpreterDB *db,
const gchar *program_path,
gchar *buffer,
gssize len,
gchar **interp_arg)
{
gchar *cp;
gchar *name;
gchar *program;
cp = strchr (buffer, '\n');
if (! cp)
cp = buffer + len - 1;
*cp = '\0';
while (cp > buffer)
{
cp--;
if ((*cp == ' ') || (*cp == '\t') || (*cp == '\r'))
*cp = '\0';
else
break;
}
for (cp = buffer + 2; (*cp == ' ') || (*cp == '\t'); cp++);
if (*cp == '\0')
return NULL;
name = cp;
for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++)
/* nothing */ ;
while ((*cp == ' ') || (*cp == '\t'))
*cp++ = '\0';
if (*cp)
{
if (strcmp ("/usr/bin/env", name) == 0)
{
program = g_hash_table_lookup (db->programs, cp);
if (program)
{
/* Shift program name and arguments to the right, if and
* only if we recorded a specific interpreter for such
* script. Otherwise let `env` tool do its job.
*/
name = cp;
for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++)
;
while ((*cp == ' ') || (*cp == '\t'))
*cp++ = '\0';
}
}
if (*cp)
*interp_arg = g_strdup (cp);
}
program = g_hash_table_lookup (db->programs, name);
if (! program)
program = name;
return g_strdup (program);
}
static gchar *
resolve_magic (PikaInterpreterDB *db,
const gchar *program_path,
gchar *buffer)
{
GSList *list;
PikaInterpreterMagic *magic;
gchar *s;
guint i;
list = db->magics;
while (list)
{
magic = list->data;
s = buffer + magic->offset;
if (magic->mask)
{
for (i = 0; i < magic->size; i++)
if ((*s++ ^ magic->magic[i]) & magic->mask[i])
break;
}
else
{
for (i = 0; i < magic->size; i++)
if ((*s++ ^ magic->magic[i]))
break;
}
if (i == magic->size)
return g_strdup (magic->program);
list = list->next;
}
return NULL;
}
gchar *
pika_interpreter_db_resolve (PikaInterpreterDB *db,
const gchar *program_path,
gchar **interp_arg)
{
GFile *file;
GInputStream *input;
gchar *program = NULL;
g_return_val_if_fail (PIKA_IS_INTERPRETER_DB (db), NULL);
g_return_val_if_fail (program_path != NULL, NULL);
g_return_val_if_fail (interp_arg != NULL, NULL);
*interp_arg = NULL;
file = g_file_new_for_path (program_path);
input = G_INPUT_STREAM (g_file_read (file, NULL, NULL));
g_object_unref (file);
if (input)
{
gsize bytes_read;
gchar buffer[BUFSIZE];
memset (buffer, 0, sizeof (buffer));
g_input_stream_read_all (input, buffer,
sizeof (buffer) - 1, /* leave one nul at the end */
&bytes_read, NULL, NULL);
g_object_unref (input);
if (bytes_read)
{
if (bytes_read > 3 && buffer[0] == '#' && buffer[1] == '!')
program = resolve_sh_bang (db, program_path, buffer, bytes_read, interp_arg);
if (! program)
program = resolve_magic (db, program_path, buffer);
}
}
if (! program)
program = resolve_extension (db, program_path);
return program;
}
static void
collect_extensions (const gchar *ext,
const gchar *program G_GNUC_UNUSED,
GString *str)
{
if (str->len)
g_string_append_c (str, G_SEARCHPATH_SEPARATOR);
g_string_append_c (str, '.');
g_string_append (str, ext);
}
/**
* pika_interpreter_db_get_extensions:
* @db:
*
* Returns: a newly allocated string with all registered file
* extensions separated by %G_SEARCHPATH_SEPARATOR;
* or %NULL if no extensions are registered
**/
gchar *
pika_interpreter_db_get_extensions (PikaInterpreterDB *db)
{
GString *str;
g_return_val_if_fail (PIKA_IS_INTERPRETER_DB (db), NULL);
if (g_hash_table_size (db->extensions) == 0)
return NULL;
str = g_string_new (NULL);
g_hash_table_foreach (db->extensions, (GHFunc) collect_extensions, str);
return g_string_free (str, FALSE);
}