PIKApp/app/plug-in/pikapluginmanager-file.c

909 lines
27 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
*
* pikapluginmanager-file.c
*
* 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/>.
*/
#include "config.h"
#include <errno.h>
#include <string.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gegl.h>
#include "libpikabase/pikabase.h"
#include "plug-in-types.h"
#include "core/pika.h"
#include "core/pika-utils.h"
#include "pikaplugin.h"
#include "pikaplugindef.h"
#include "pikapluginmanager.h"
#include "pikapluginmanager-file.h"
#include "pikapluginprocedure.h"
#include "pika-log.h"
#include "pika-intl.h"
#ifdef G_OS_WIN32
#include <stdlib.h>
#endif
typedef enum
{
/* positive values indicate the length of a matching magic */
FILE_MATCH_NONE = 0,
FILE_MATCH_SIZE = -1
} FileMatchType;
/* local function prototypes */
static gboolean file_proc_in_group (PikaPlugInProcedure *file_proc,
PikaFileProcedureGroup group);
static PikaPlugInProcedure * file_proc_find (GSList *procs,
GFile *file,
GError **error);
static PikaPlugInProcedure * file_proc_find_by_prefix (GSList *procs,
GFile *file,
gboolean skip_magic);
static PikaPlugInProcedure * file_proc_find_by_extension (GSList *procs,
GFile *file,
gboolean skip_magic);
static PikaPlugInProcedure * file_proc_find_by_mime_type (GSList *procs,
const gchar *mime_type);
static PikaPlugInProcedure * file_proc_find_by_name (GSList *procs,
GFile *file,
gboolean skip_magic);
static void file_convert_string (const gchar *instr,
gchar *outmem,
gint maxmem,
gint *nmem);
static FileMatchType file_check_single_magic (const gchar *offset,
const gchar *type,
const gchar *value,
const guchar *file_head,
gint headsize,
GFile *file,
GInputStream *input);
static FileMatchType file_check_magic_list (GSList *magics_list,
const guchar *head,
gint headsize,
GFile *file,
GInputStream *input);
/* public functions */
void
pika_plug_in_manager_add_load_procedure (PikaPlugInManager *manager,
PikaPlugInProcedure *proc)
{
g_return_if_fail (PIKA_IS_PLUG_IN_MANAGER (manager));
g_return_if_fail (PIKA_IS_PLUG_IN_PROCEDURE (proc));
if (! g_slist_find (manager->load_procs, proc))
manager->load_procs = g_slist_prepend (manager->load_procs, proc);
}
void
pika_plug_in_manager_add_save_procedure (PikaPlugInManager *manager,
PikaPlugInProcedure *proc)
{
g_return_if_fail (PIKA_IS_PLUG_IN_MANAGER (manager));
g_return_if_fail (PIKA_IS_PLUG_IN_PROCEDURE (proc));
if (file_proc_in_group (proc, PIKA_FILE_PROCEDURE_GROUP_SAVE))
{
if (! g_slist_find (manager->save_procs, proc))
manager->save_procs = g_slist_prepend (manager->save_procs, proc);
}
if (file_proc_in_group (proc, PIKA_FILE_PROCEDURE_GROUP_EXPORT))
{
if (! g_slist_find (manager->export_procs, proc))
manager->export_procs = g_slist_prepend (manager->export_procs, proc);
}
}
GSList *
pika_plug_in_manager_get_file_procedures (PikaPlugInManager *manager,
PikaFileProcedureGroup group)
{
g_return_val_if_fail (PIKA_IS_PLUG_IN_MANAGER (manager), NULL);
switch (group)
{
case PIKA_FILE_PROCEDURE_GROUP_NONE:
return NULL;
case PIKA_FILE_PROCEDURE_GROUP_OPEN:
return manager->display_load_procs;
case PIKA_FILE_PROCEDURE_GROUP_SAVE:
return manager->display_save_procs;
case PIKA_FILE_PROCEDURE_GROUP_EXPORT:
return manager->display_export_procs;
default:
g_return_val_if_reached (NULL);
}
}
PikaPlugInProcedure *
pika_plug_in_manager_file_procedure_find (PikaPlugInManager *manager,
PikaFileProcedureGroup group,
GFile *file,
GError **error)
{
g_return_val_if_fail (PIKA_IS_PLUG_IN_MANAGER (manager), NULL);
g_return_val_if_fail (G_IS_FILE (file), NULL);
g_return_val_if_fail (error == NULL || *error == NULL, NULL);
switch (group)
{
case PIKA_FILE_PROCEDURE_GROUP_OPEN:
return file_proc_find (manager->load_procs, file, error);
case PIKA_FILE_PROCEDURE_GROUP_SAVE:
return file_proc_find (manager->save_procs, file, error);
case PIKA_FILE_PROCEDURE_GROUP_EXPORT:
return file_proc_find (manager->export_procs, file, error);
default:
g_return_val_if_reached (NULL);
}
}
PikaPlugInProcedure *
pika_plug_in_manager_file_procedure_find_by_prefix (PikaPlugInManager *manager,
PikaFileProcedureGroup group,
GFile *file)
{
g_return_val_if_fail (PIKA_IS_PLUG_IN_MANAGER (manager), NULL);
g_return_val_if_fail (G_IS_FILE (file), NULL);
switch (group)
{
case PIKA_FILE_PROCEDURE_GROUP_OPEN:
return file_proc_find_by_prefix (manager->load_procs, file, FALSE);
case PIKA_FILE_PROCEDURE_GROUP_SAVE:
return file_proc_find_by_prefix (manager->save_procs, file, FALSE);
case PIKA_FILE_PROCEDURE_GROUP_EXPORT:
return file_proc_find_by_prefix (manager->export_procs, file, FALSE);
default:
g_return_val_if_reached (NULL);
}
}
PikaPlugInProcedure *
pika_plug_in_manager_file_procedure_find_by_extension (PikaPlugInManager *manager,
PikaFileProcedureGroup group,
GFile *file)
{
g_return_val_if_fail (PIKA_IS_PLUG_IN_MANAGER (manager), NULL);
g_return_val_if_fail (G_IS_FILE (file), NULL);
switch (group)
{
case PIKA_FILE_PROCEDURE_GROUP_OPEN:
return file_proc_find_by_extension (manager->load_procs, file, FALSE);
case PIKA_FILE_PROCEDURE_GROUP_SAVE:
return file_proc_find_by_extension (manager->save_procs, file, FALSE);
case PIKA_FILE_PROCEDURE_GROUP_EXPORT:
return file_proc_find_by_extension (manager->export_procs, file, FALSE);
default:
g_return_val_if_reached (NULL);
}
}
PikaPlugInProcedure *
pika_plug_in_manager_file_procedure_find_by_mime_type (PikaPlugInManager *manager,
PikaFileProcedureGroup group,
const gchar *mime_type)
{
g_return_val_if_fail (PIKA_IS_PLUG_IN_MANAGER (manager), NULL);
g_return_val_if_fail (mime_type != NULL, NULL);
switch (group)
{
case PIKA_FILE_PROCEDURE_GROUP_OPEN:
return file_proc_find_by_mime_type (manager->load_procs, mime_type);
case PIKA_FILE_PROCEDURE_GROUP_SAVE:
return file_proc_find_by_mime_type (manager->save_procs, mime_type);
case PIKA_FILE_PROCEDURE_GROUP_EXPORT:
return file_proc_find_by_mime_type (manager->export_procs, mime_type);
default:
g_return_val_if_reached (NULL);
}
}
/* private functions */
static gboolean
file_proc_in_group (PikaPlugInProcedure *file_proc,
PikaFileProcedureGroup group)
{
const gchar *name = pika_object_get_name (file_proc);
gboolean is_xcf_save = FALSE;
gboolean is_filter = FALSE;
is_xcf_save = (strcmp (name, "pika-xcf-save") == 0);
is_filter = (strcmp (name, "file-gz-save") == 0 ||
strcmp (name, "file-bz2-save") == 0 ||
strcmp (name, "file-xz-save") == 0);
switch (group)
{
case PIKA_FILE_PROCEDURE_GROUP_NONE:
return FALSE;
case PIKA_FILE_PROCEDURE_GROUP_SAVE:
/* Only .xcf shall pass */
return is_xcf_save || is_filter;
case PIKA_FILE_PROCEDURE_GROUP_EXPORT:
/* Anything but .xcf shall pass */
return ! is_xcf_save;
case PIKA_FILE_PROCEDURE_GROUP_OPEN:
/* No filter applied for Open */
return TRUE;
default:
case PIKA_FILE_PROCEDURE_GROUP_ANY:
return TRUE;
}
}
static PikaPlugInProcedure *
file_proc_find (GSList *procs,
GFile *file,
GError **error)
{
PikaPlugInProcedure *file_proc;
PikaPlugInProcedure *size_matched_proc = NULL;
gint size_match_count = 0;
g_return_val_if_fail (procs != NULL, NULL);
g_return_val_if_fail (G_IS_FILE (file), NULL);
g_return_val_if_fail (error == NULL || *error == NULL, NULL);
/* First, check magicless prefixes/suffixes */
file_proc = file_proc_find_by_name (procs, file, TRUE);
if (file_proc)
return file_proc;
/* Then look for magics, but not on remote files */
if (g_file_is_native (file))
{
GSList *list;
GInputStream *input = NULL;
gboolean opened = FALSE;
gsize head_size = 0;
guchar head[256];
FileMatchType best_match_val = FILE_MATCH_NONE;
PikaPlugInProcedure *best_file_proc = NULL;
for (list = procs; list; list = g_slist_next (list))
{
file_proc = list->data;
if (file_proc->magics_list)
{
if (G_UNLIKELY (! opened))
{
input = G_INPUT_STREAM (g_file_read (file, NULL, error));
if (input)
{
g_input_stream_read_all (input,
head, sizeof (head),
&head_size, NULL, error);
if (head_size < 4)
{
g_object_unref (input);
input = NULL;
}
else
{
GDataInputStream *data_input;
data_input = g_data_input_stream_new (input);
g_object_unref (input);
input = G_INPUT_STREAM (data_input);
}
}
opened = TRUE;
}
if (head_size >= 4)
{
FileMatchType match_val;
match_val = file_check_magic_list (file_proc->magics_list,
head, head_size,
file, input);
if (match_val == FILE_MATCH_SIZE)
{
/* Use it only if no other magic matches */
size_match_count++;
size_matched_proc = file_proc;
}
else if (match_val != FILE_MATCH_NONE)
{
PIKA_LOG (MAGIC_MATCH,
"magic match %d on %s\n",
match_val,
pika_object_get_name (file_proc));
if (match_val > best_match_val)
{
best_match_val = match_val;
best_file_proc = file_proc;
}
}
}
}
}
if (input)
g_object_unref (input);
if (best_file_proc)
{
PIKA_LOG (MAGIC_MATCH,
"best magic match on %s\n",
pika_object_get_name (best_file_proc));
return best_file_proc;
}
}
if (size_match_count == 1)
return size_matched_proc;
/* As a last resort, try matching by name, not skipping magic procs */
file_proc = file_proc_find_by_name (procs, file, FALSE);
if (file_proc)
{
/* we found a procedure, clear error that might have been set */
g_clear_error (error);
}
else
{
/* set an error message unless one was already set */
if (error && *error == NULL)
g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
_("Unknown file type"));
}
return file_proc;
}
static PikaPlugInProcedure *
file_proc_find_by_mime_type (GSList *procs,
const gchar *mime_type)
{
GSList *list;
g_return_val_if_fail (mime_type != NULL, NULL);
for (list = procs; list; list = g_slist_next (list))
{
PikaPlugInProcedure *proc = list->data;
GSList *mime;
for (mime = proc->mime_types_list; mime; mime = g_slist_next (mime))
{
if (! strcmp (mime_type, mime->data))
return proc;
}
}
return NULL;
}
static PikaPlugInProcedure *
file_proc_find_by_prefix (GSList *procs,
GFile *file,
gboolean skip_magic)
{
gchar *uri = g_file_get_uri (file);
GSList *p;
for (p = procs; p; p = g_slist_next (p))
{
PikaPlugInProcedure *proc = p->data;
GSList *prefixes;
if (skip_magic && proc->magics_list)
continue;
for (prefixes = proc->prefixes_list;
prefixes;
prefixes = g_slist_next (prefixes))
{
if (g_str_has_prefix (uri, prefixes->data))
{
g_free (uri);
return proc;
}
}
}
g_free (uri);
return NULL;
}
static PikaPlugInProcedure *
file_proc_find_by_extension (GSList *procs,
GFile *file,
gboolean skip_magic)
{
gchar *ext = pika_file_get_extension (file);
if (ext)
{
GSList *p;
for (p = procs; p; p = g_slist_next (p))
{
PikaPlugInProcedure *proc = p->data;
if (skip_magic && proc->magics_list)
continue;
if (g_slist_find_custom (proc->extensions_list,
ext + 1,
(GCompareFunc) g_ascii_strcasecmp))
{
g_free (ext);
return proc;
}
}
g_free (ext);
}
return NULL;
}
static PikaPlugInProcedure *
file_proc_find_by_name (GSList *procs,
GFile *file,
gboolean skip_magic)
{
PikaPlugInProcedure *proc;
proc = file_proc_find_by_prefix (procs, file, skip_magic);
if (! proc)
proc = file_proc_find_by_extension (procs, file, skip_magic);
return proc;
}
static void
file_convert_string (const gchar *instr,
gchar *outmem,
gint maxmem,
gint *nmem)
{
/* Convert a string in C-notation to array of char */
const guchar *uin = (const guchar *) instr;
guchar *uout = (guchar *) outmem;
guchar tmp[5], *tmpptr;
guint k;
while ((*uin != '\0') && ((((gchar *) uout) - outmem) < maxmem))
{
if (*uin != '\\') /* Not an escaped character ? */
{
*(uout++) = *(uin++);
continue;
}
if (*(++uin) == '\0')
{
*(uout++) = '\\';
break;
}
switch (*uin)
{
case '0': case '1': case '2': case '3': /* octal */
for (tmpptr = tmp; (tmpptr - tmp) <= 3;)
{
*(tmpptr++) = *(uin++);
if ( (*uin == '\0') || (!g_ascii_isdigit (*uin))
|| (*uin == '8') || (*uin == '9'))
break;
}
*tmpptr = '\0';
sscanf ((gchar *) tmp, "%o", &k);
*(uout++) = k;
break;
case 'a': *(uout++) = '\a'; uin++; break;
case 'b': *(uout++) = '\b'; uin++; break;
case 't': *(uout++) = '\t'; uin++; break;
case 'n': *(uout++) = '\n'; uin++; break;
case 'v': *(uout++) = '\v'; uin++; break;
case 'f': *(uout++) = '\f'; uin++; break;
case 'r': *(uout++) = '\r'; uin++; break;
default : *(uout++) = *(uin++); break;
}
}
*nmem = ((gchar *) uout) - outmem;
}
static FileMatchType
file_check_single_magic (const gchar *offset,
const gchar *type,
const gchar *value,
const guchar *file_head,
gint headsize,
GFile *file,
GInputStream *input)
{
FileMatchType found = FILE_MATCH_NONE;
glong offs;
gulong num_testval;
gulong num_operator_val;
gint numbytes, k;
const gchar *num_operator_ptr;
gchar num_operator;
/* Check offset */
if (sscanf (offset, "%ld", &offs) != 1)
return FILE_MATCH_NONE;
/* Check type of test */
num_operator_ptr = NULL;
num_operator = '\0';
if (g_str_has_prefix (type, "byte"))
{
numbytes = 1;
num_operator_ptr = type + strlen ("byte");
}
else if (g_str_has_prefix (type, "short"))
{
numbytes = 2;
num_operator_ptr = type + strlen ("short");
}
else if (g_str_has_prefix (type, "long"))
{
numbytes = 4;
num_operator_ptr = type + strlen ("long");
}
else if (g_str_has_prefix (type, "size"))
{
numbytes = 5;
}
else if (strcmp (type, "string") == 0)
{
numbytes = 0;
}
else
{
return FILE_MATCH_NONE;
}
/* Check numerical operator value if present */
if (num_operator_ptr && (*num_operator_ptr == '&'))
{
if (g_ascii_isdigit (num_operator_ptr[1]))
{
if (num_operator_ptr[1] != '0') /* decimal */
sscanf (num_operator_ptr+1, "%lu", &num_operator_val);
else if (num_operator_ptr[2] == 'x') /* hexadecimal */
sscanf (num_operator_ptr+3, "%lx", &num_operator_val);
else /* octal */
sscanf (num_operator_ptr+2, "%lo", &num_operator_val);
num_operator = *num_operator_ptr;
}
}
if (numbytes > 0)
{
/* Numerical test */
gchar num_test = '=';
gulong fileval = 0;
/* Check test value */
if ((value[0] == '>') || (value[0] == '<'))
{
num_test = value[0];
value++;
}
errno = 0;
num_testval = strtol (value, NULL, 0);
if (errno != 0)
return FILE_MATCH_NONE;
if (numbytes == 5)
{
/* Check for file size */
GFileInfo *info = g_file_query_info (file,
G_FILE_ATTRIBUTE_STANDARD_SIZE,
G_FILE_QUERY_INFO_NONE,
NULL, NULL);
if (! info)
return FILE_MATCH_NONE;
fileval = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_STANDARD_SIZE);
g_object_unref (info);
}
else if (offs >= 0 &&
(offs + numbytes <= headsize))
{
/* We have it in memory */
for (k = 0; k < numbytes; k++)
fileval = (fileval << 8) | (glong) file_head[offs + k];
}
else
{
/* Read it from file */
if (! g_seekable_seek (G_SEEKABLE (input), offs,
(offs >= 0) ? G_SEEK_SET : G_SEEK_END,
NULL, NULL))
return FILE_MATCH_NONE;
for (k = 0; k < numbytes; k++)
{
guchar byte;
GError *error = NULL;
byte = g_data_input_stream_read_byte (G_DATA_INPUT_STREAM (input),
NULL, &error);
if (error)
{
g_clear_error (&error);
return FILE_MATCH_NONE;
}
fileval = (fileval << 8) | byte;
}
}
if (num_operator == '&')
fileval &= num_operator_val;
if (num_test == '<')
{
if (fileval < num_testval)
found = numbytes;
}
else if (num_test == '>')
{
if (fileval > num_testval)
found = numbytes;
}
else
{
if (fileval == num_testval)
found = numbytes;
}
if (found && (numbytes == 5))
found = FILE_MATCH_SIZE;
}
else if (numbytes == 0)
{
/* String test */
gchar mem_testval[256];
file_convert_string (value,
mem_testval, sizeof (mem_testval),
&numbytes);
if (numbytes <= 0)
return FILE_MATCH_NONE;
if (offs >= 0 &&
(offs + numbytes <= headsize))
{
/* We have it in memory */
if (memcmp (mem_testval, file_head + offs, numbytes) == 0)
found = numbytes;
}
else
{
/* Read it from file */
if (! g_seekable_seek (G_SEEKABLE (input), offs,
(offs >= 0) ? G_SEEK_SET : G_SEEK_END,
NULL, NULL))
return FILE_MATCH_NONE;
for (k = 0; k < numbytes; k++)
{
guchar byte;
GError *error = NULL;
byte = g_data_input_stream_read_byte (G_DATA_INPUT_STREAM (input),
NULL, &error);
if (error)
{
g_clear_error (&error);
return FILE_MATCH_NONE;
}
if (byte != mem_testval[k])
return FILE_MATCH_NONE;
}
found = numbytes;
}
}
return found;
}
static FileMatchType
file_check_magic_list (GSList *magics_list,
const guchar *head,
gint headsize,
GFile *file,
GInputStream *input)
{
gboolean and = FALSE;
gboolean found = FALSE;
FileMatchType best_match_val = FILE_MATCH_NONE;
FileMatchType match_val = FILE_MATCH_NONE;
for (; magics_list; magics_list = magics_list->next)
{
const gchar *offset;
const gchar *type;
const gchar *value;
FileMatchType single_match_val = FILE_MATCH_NONE;
if ((offset = magics_list->data) == NULL) return FILE_MATCH_NONE;
if ((magics_list = magics_list->next) == NULL) return FILE_MATCH_NONE;
if ((type = magics_list->data) == NULL) return FILE_MATCH_NONE;
if ((magics_list = magics_list->next) == NULL) return FILE_MATCH_NONE;
if ((value = magics_list->data) == NULL) return FILE_MATCH_NONE;
single_match_val = file_check_single_magic (offset, type, value,
head, headsize,
file, input);
if (and)
found = found && (single_match_val != FILE_MATCH_NONE);
else
found = (single_match_val != FILE_MATCH_NONE);
if (found)
{
if (match_val == FILE_MATCH_NONE)
{
/* if we have no match yet, this is it in any case */
match_val = single_match_val;
}
else if (single_match_val != FILE_MATCH_NONE)
{
/* else if we have a match on this one, combine it with the
* existing return value
*/
if (single_match_val == FILE_MATCH_SIZE)
{
/* if we already have a magic match, simply increase
* that by one to indicate "better match", not perfect
* but better than losing the additional size match
* entirely
*/
if (match_val != FILE_MATCH_SIZE)
match_val += 1;
}
else
{
/* if we already have a magic match, simply add to its
* length; otherwise if we already have a size match,
* combine it with this match, see comment above
*/
if (match_val != FILE_MATCH_SIZE)
match_val += single_match_val;
else
match_val = single_match_val + 1;
}
}
}
else
{
match_val = FILE_MATCH_NONE;
}
and = (strchr (offset, '&') != NULL);
if (! and)
{
/* when done with this 'and' list, update best_match_val */
if (best_match_val == FILE_MATCH_NONE)
{
/* if we have no best match yet, this is it */
best_match_val = match_val;
}
else if (match_val != FILE_MATCH_NONE)
{
/* otherwise if this was a match, update the best match, note
* that by using MAX we will not overwrite a magic match
* with a size match
*/
best_match_val = MAX (best_match_val, match_val);
}
match_val = FILE_MATCH_NONE;
}
}
return best_match_val;
}