PIKApp/app/propgui/pikapropgui-eval.c

1048 lines
30 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-1997 Spencer Kimball and Peter Mattis
*
* pikapropgui-eval.c
* Copyright (C) 2017 Ell
*
* 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 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Less General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* This is a simple interpreter for the GUM language (the GEGL UI Meta
* language), used in certain property keys of GEGL operations. What follows
* is a hand-wavy summary of the syntax and semantics (no BNF for you!)
*
* There are currently two types of expressions:
*
* Boolean expressions
* -------------------
*
* There are three types of simple boolean expressions:
*
* - Literal: Either `0` or `1`, evaluating to FALSE and TRUE, respectively.
*
* - Reference: Has the form `$key` or `$property.key`. Evaluates to the
* value of key `key`, which should itself be a boolean expression. In
* the first form, `key` refers to a key of the same property to which the
* currently-evaluated key belongs. In the second form, `key` refers to a
* key of `property`.
*
* - Dependency: Dependencies begin with the name of a property, on which
* the result depends. Currently supported property types are:
*
* - Boolean: The expression consists of the property name alone, and
* its value is the value of the property.
*
* - Enum: The property name shall be followed by a brace-enclosed,
* comma-separated list of enum values, given as nicks. The expression
* evaluates to TRUE iff the property matches any of the values.
*
* Complex boolean expressions can be formed using `!` (negation), `&`
* (conjunction), `|` (disjunction), and parentheses (grouping), following the
* usual precedence rules.
*
* String expressions
* ------------------
*
* There are three types of simple string expressions:
*
* - Literal: A string literal, surrounded by single quotes (`'`). Special
* characters (in particular, single quotes) can be escaped using a
* backslash (`\`).
*
* - Reference: Same as a boolean reference, but should refer to a key
* containing a string expression.
*
* - Deferred literal: Names a key, in the same fashion as a reference, but
* without the leading `$`. The value of this key is taken literally as
* the value of the expression. Deferred literals should usually be
* favored over inline string literals, because they can be translated
* independently of the expression.
*
* Currently, the only complex string expression is string selection: It has
* the form of a bracket-enclosed, comma-separated list of expressions of the
* form `<condition> : <value>`, where `<condition>` is a boolean expression,
* and `<value>` is a string expression. The result of the expression is the
* associated value of the first condition that evaluates to TRUE. If no
* condition is met, the result is NULL.
*
*
* Whitespace separating subexpressions is insignificant.
*/
#include "config.h"
#include <string.h>
#include <gegl.h>
#include <gegl-paramspecs.h>
#include <gtk/gtk.h>
#include "propgui-types.h"
#include "pikapropgui-eval.h"
typedef enum
{
PIKA_PROP_EVAL_FAILED /* generic error condition */
} PikaPropEvalErrorCode;
static gboolean pika_prop_eval_boolean_impl (GObject *config,
GParamSpec *pspec,
const gchar *key,
gint default_value,
GError **error,
gint depth);
static gboolean pika_prop_eval_boolean_or (GObject *config,
GParamSpec *pspec,
const gchar **expr,
gchar **t,
GError **error,
gint depth);
static gboolean pika_prop_eval_boolean_and (GObject *config,
GParamSpec *pspec,
const gchar **expr,
gchar **t,
GError **error,
gint depth);
static gboolean pika_prop_eval_boolean_not (GObject *config,
GParamSpec *pspec,
const gchar **expr,
gchar **t,
GError **error,
gint depth);
static gboolean pika_prop_eval_boolean_group (GObject *config,
GParamSpec *pspec,
const gchar **expr,
gchar **t,
GError **error,
gint depth);
static gboolean pika_prop_eval_boolean_simple (GObject *config,
GParamSpec *pspec,
const gchar **expr,
gchar **t,
GError **error,
gint depth);
static gchar * pika_prop_eval_string_impl (GObject *config,
GParamSpec *pspec,
const gchar *key,
const gchar *default_value,
GError **error,
gint depth);
static gchar * pika_prop_eval_string_selection (GObject *config,
GParamSpec *pspec,
const gchar **expr,
gchar **t,
GError **error,
gint depth);
static gchar * pika_prop_eval_string_simple (GObject *config,
GParamSpec *pspec,
const gchar **expr,
gchar **t,
GError **error,
gint depth);
static gboolean pika_prop_eval_parse_reference (GObject *config,
GParamSpec *pspec,
const gchar **expr,
gchar **t,
GError **error,
GParamSpec **ref_pspec,
gchar **ref_key);
static gboolean pika_prop_eval_depth_test (gint depth,
GError **error);
static gchar * pika_prop_eval_read_token (const gchar **expr,
gchar **t,
GError **error);
static gboolean pika_prop_eval_is_name (const gchar *token);
#define PIKA_PROP_EVAL_ERROR (pika_prop_eval_error_quark ())
static GQuark pika_prop_eval_error_quark (void);
/* public functions */
gboolean
pika_prop_eval_boolean (GObject *config,
GParamSpec *pspec,
const gchar *key,
gboolean default_value)
{
GError *error = NULL;
gboolean result;
result = pika_prop_eval_boolean_impl (config, pspec,
key, default_value, &error, 0);
if (error)
{
g_warning ("in object of type '%s': %s",
G_OBJECT_TYPE_NAME (config),
error->message);
g_error_free (error);
return default_value;
}
return result;
}
gchar *
pika_prop_eval_string (GObject *config,
GParamSpec *pspec,
const gchar *key,
const gchar *default_value)
{
GError *error = NULL;
gchar *result;
result = pika_prop_eval_string_impl (config, pspec,
key, default_value, &error, 0);
if (error)
{
g_warning ("in object of type '%s': %s",
G_OBJECT_TYPE_NAME (config),
error->message);
g_error_free (error);
return g_strdup (default_value);
}
return result;
}
/* private functions */
static gboolean
pika_prop_eval_boolean_impl (GObject *config,
GParamSpec *pspec,
const gchar *key,
gint default_value,
GError **error,
gint depth)
{
const gchar *expr;
gchar *t = NULL;
gboolean result = FALSE;
if (! pika_prop_eval_depth_test (depth, error))
return FALSE;
expr = gegl_param_spec_get_property_key (pspec, key);
if (! expr)
{
/* we use `default_value < 0` to specify that the key must exist */
if (default_value < 0)
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"key '%s' of property '%s' not found",
key,
g_param_spec_get_name (pspec));
return FALSE;
}
return default_value;
}
pika_prop_eval_read_token (&expr, &t, error);
if (! *error)
{
result = pika_prop_eval_boolean_or (config, pspec,
&expr, &t, error, depth);
}
/* check for trailing tokens at the end of the expression */
if (! *error && t)
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"invalid expression");
}
g_free (t);
if (*error)
{
g_prefix_error (error,
"in key '%s' of property '%s': ",
key,
g_param_spec_get_name (pspec));
return FALSE;
}
return result;
}
static gboolean
pika_prop_eval_boolean_or (GObject *config,
GParamSpec *pspec,
const gchar **expr,
gchar **t,
GError **error,
gint depth)
{
gboolean result;
if (! pika_prop_eval_depth_test (depth, error))
return FALSE;
result = pika_prop_eval_boolean_and (config, pspec,
expr, t, error, depth);
while (! *error && ! g_strcmp0 (*t, "|"))
{
pika_prop_eval_read_token (expr, t, error);
if (*error)
return FALSE;
/* keep evaluating even if `result` is TRUE, because we still need to
* parse the rest of the subexpression.
*/
result |= pika_prop_eval_boolean_and (config, pspec,
expr, t, error, depth);
}
return result;
}
static gboolean
pika_prop_eval_boolean_and (GObject *config,
GParamSpec *pspec,
const gchar **expr,
gchar **t,
GError **error,
gint depth)
{
gboolean result;
if (! pika_prop_eval_depth_test (depth, error))
return FALSE;
result = pika_prop_eval_boolean_not (config, pspec,
expr, t, error, depth);
while (! *error && ! g_strcmp0 (*t, "&"))
{
pika_prop_eval_read_token (expr, t, error);
if (*error)
return FALSE;
/* keep evaluating even if `result` is FALSE, because we still need to
* parse the rest of the subexpression.
*/
result &= pika_prop_eval_boolean_not (config, pspec,
expr, t, error, depth);
}
return result;
}
static gboolean
pika_prop_eval_boolean_not (GObject *config,
GParamSpec *pspec,
const gchar **expr,
gchar **t,
GError **error,
gint depth)
{
if (! pika_prop_eval_depth_test (depth, error))
return FALSE;
if (! g_strcmp0 (*t, "!"))
{
pika_prop_eval_read_token (expr, t, error);
if (*error)
return FALSE;
return ! pika_prop_eval_boolean_not (config, pspec,
expr, t, error, depth + 1);
}
return pika_prop_eval_boolean_group (config, pspec,
expr, t, error, depth);
}
static gboolean
pika_prop_eval_boolean_group (GObject *config,
GParamSpec *pspec,
const gchar **expr,
gchar **t,
GError **error,
gint depth)
{
if (! pika_prop_eval_depth_test (depth, error))
return FALSE;
if (! g_strcmp0 (*t, "("))
{
gboolean result;
pika_prop_eval_read_token (expr, t, error);
if (*error)
return FALSE;
result = pika_prop_eval_boolean_or (config, pspec,
expr, t, error, depth + 1);
if (*error)
return FALSE;
if (g_strcmp0 (*t, ")"))
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"unterminated group");
return FALSE;
}
pika_prop_eval_read_token (expr, t, error);
return result;
}
return pika_prop_eval_boolean_simple (config, pspec,
expr, t, error, depth);
}
static gboolean
pika_prop_eval_boolean_simple (GObject *config,
GParamSpec *pspec,
const gchar **expr,
gchar **t,
GError **error,
gint depth)
{
gboolean result;
if (! pika_prop_eval_depth_test (depth, error))
return FALSE;
if (! *t)
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"invalid expression");
return FALSE;
}
/* literal */
if (! strcmp (*t, "0"))
{
result = FALSE;
pika_prop_eval_read_token (expr, t, error);
}
else if (! strcmp (*t, "1"))
{
result = TRUE;
pika_prop_eval_read_token (expr, t, error);
}
/* reference */
else if (! strcmp (*t, "$"))
{
gchar *key;
pika_prop_eval_read_token (expr, t, error);
if (*error)
return FALSE;
if (! pika_prop_eval_parse_reference (config, pspec,
expr, t, error, &pspec, &key))
return FALSE;
result = pika_prop_eval_boolean_impl (config, pspec,
key, -1, error, depth + 1);
g_free (key);
}
/* dependency */
else if (pika_prop_eval_is_name (*t))
{
const gchar *property_name;
GParamSpec *pspec;
GType type;
property_name = *t;
pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (config),
property_name);
if (! pspec)
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"property '%s' not found",
property_name);
return TRUE;
}
property_name = g_param_spec_get_name (pspec);
type = G_PARAM_SPEC_VALUE_TYPE (pspec);
if (g_type_is_a (type, G_TYPE_BOOLEAN))
{
g_object_get (config, property_name, &result, NULL);
}
else if (g_type_is_a (type, G_TYPE_ENUM))
{
GEnumClass *enum_class;
gint value;
pika_prop_eval_read_token (expr, t, error);
if (*error)
return FALSE;
if (g_strcmp0 (*t , "{"))
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"missing enum value set "
"for property '%s'",
property_name);
return FALSE;
}
enum_class = g_type_class_peek (type);
g_object_get (config, property_name, &value, NULL);
result = FALSE;
while (pika_prop_eval_read_token (expr, t, error) &&
pika_prop_eval_is_name (*t))
{
const gchar *nick;
GEnumValue *enum_value;
nick = *t;
enum_value = g_enum_get_value_by_nick (enum_class, nick);
if (! enum_value)
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"invalid enum value '%s' "
"for property '%s'",
nick,
property_name);
return FALSE;
}
if (value == enum_value->value)
result = TRUE;
pika_prop_eval_read_token (expr, t, error);
if (*error)
return FALSE;
if (! g_strcmp0 (*t, ","))
{
continue;
}
else if (! g_strcmp0 (*t, "}"))
{
break;
}
else
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"invalid enum value set "
"for property '%s'",
property_name);
return FALSE;
}
}
if (*error)
return FALSE;
if (g_strcmp0 (*t, "}"))
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"unterminated enum value set "
"for property '%s'",
property_name);
return FALSE;
}
}
else
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"invalid type "
"for property '%s'",
property_name);
return FALSE;
}
pika_prop_eval_read_token (expr, t, error);
}
else
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"invalid expression");
return FALSE;
}
return result;
}
static gchar *
pika_prop_eval_string_impl (GObject *config,
GParamSpec *pspec,
const gchar *key,
const gchar *default_value,
GError **error,
gint depth)
{
const gchar *expr;
gchar *t = NULL;
gchar *result = NULL;
if (! pika_prop_eval_depth_test (depth, error))
return NULL;
expr = gegl_param_spec_get_property_key (pspec, key);
if (! expr)
return g_strdup (default_value);
pika_prop_eval_read_token (&expr, &t, error);
if (! *error)
{
result = pika_prop_eval_string_selection (config, pspec,
&expr, &t, error, depth);
}
/* check for trailing tokens at the end of the expression */
if (! *error && t)
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"invalid expression");
g_clear_pointer (&result, g_free);
}
g_free (t);
if (*error)
{
g_prefix_error (error,
"in key '%s' of property '%s': ",
key,
g_param_spec_get_name (pspec));
return NULL;
}
if (result)
return result;
else
return g_strdup (default_value);
}
static gchar *
pika_prop_eval_string_selection (GObject *config,
GParamSpec *pspec,
const gchar **expr,
gchar **t,
GError **error,
gint depth)
{
if (! pika_prop_eval_depth_test (depth, error) || ! t)
return NULL;
if (! g_strcmp0 (*t, "["))
{
gboolean match = FALSE;
gchar *result = NULL;
if (! g_strcmp0 (pika_prop_eval_read_token (expr, t, error), "]"))
return NULL;
while (! *error)
{
gboolean cond;
gchar *value;
cond = pika_prop_eval_boolean_or (config, pspec,
expr, t, error, depth + 1);
if (*error)
break;
if (g_strcmp0 (*t, ":"))
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"missing string selection value");
break;
}
pika_prop_eval_read_token (expr, t, error);
if (*error)
break;
value = pika_prop_eval_string_selection (config, pspec,
expr, t, error, depth + 1);
if (*error)
break;
if (! match && cond)
{
match = TRUE;
result = value;
}
else
{
g_free (value);
}
if (! g_strcmp0 (*t, ","))
{
pika_prop_eval_read_token (expr, t, error);
continue;
}
else if (! g_strcmp0 (*t, "]"))
{
pika_prop_eval_read_token (expr, t, error);
break;
}
else
{
if (*t)
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"invalid string selection");
}
else
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"unterminated string selection");
}
break;
}
}
if (*error)
{
g_free (result);
return NULL;
}
return result;
}
return pika_prop_eval_string_simple (config,
pspec, expr, t, error, depth);
}
static gchar *
pika_prop_eval_string_simple (GObject *config,
GParamSpec *pspec,
const gchar **expr,
gchar **t,
GError **error,
gint depth)
{
gchar *result = NULL;
if (! pika_prop_eval_depth_test (depth, error))
return NULL;
/* literal */
if (*t && **t == '\'')
{
gchar *escaped;
escaped = g_strndup (*t + 1, strlen (*t + 1) - 1);
result = g_strcompress (escaped);
g_free (escaped);
pika_prop_eval_read_token (expr, t, error);
if (*error)
{
g_free (result);
return NULL;
}
}
/* reference */
else if (! g_strcmp0 (*t, "$"))
{
gchar *key;
pika_prop_eval_read_token (expr, t, error);
if (*error)
return NULL;
if (! pika_prop_eval_parse_reference (config, pspec,
expr, t, error, &pspec, &key))
return NULL;
result = pika_prop_eval_string_impl (config, pspec,
key, NULL, error, depth + 1);
g_free (key);
}
/* deferred literal */
else if (pika_prop_eval_is_name (*t))
{
GParamSpec *str_pspec;
gchar *str_key;
const gchar *str;
if (! pika_prop_eval_parse_reference (config, pspec,
expr, t, error,
&str_pspec, &str_key))
return NULL;
str = gegl_param_spec_get_property_key (str_pspec, str_key);
if (! str)
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"key '%s' of property '%s' not found",
str_key,
g_param_spec_get_name (str_pspec));
g_free (str_key);
return NULL;
}
g_free (str_key);
result = g_strdup (str);
}
else
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"invalid expression");
return NULL;
}
return result;
}
static gboolean
pika_prop_eval_parse_reference (GObject *config,
GParamSpec *pspec,
const gchar **expr,
gchar **t,
GError **error,
GParamSpec **ref_pspec,
gchar **ref_key)
{
if (! pika_prop_eval_is_name (*t))
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"invalid reference");
return FALSE;
}
*ref_pspec = pspec;
*ref_key = g_strdup (*t);
pika_prop_eval_read_token (expr, t, error);
if (*error)
{
g_free (*ref_key);
return FALSE;
}
if (! g_strcmp0 (*t, "."))
{
gchar *property_name;
property_name = *ref_key;
if (! pika_prop_eval_read_token (expr, t, error) ||
! pika_prop_eval_is_name (*t))
{
if (! *error)
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"invalid reference");
}
g_free (property_name);
return FALSE;
}
*ref_pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (config),
property_name);
if (! *ref_pspec)
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"property '%s' not found",
property_name);
g_free (property_name);
return FALSE;
}
g_free (property_name);
*ref_key = g_strdup (*t);
pika_prop_eval_read_token (expr, t, error);
if (*error)
{
g_free (*ref_key);
return FALSE;
}
}
return TRUE;
}
static gboolean
pika_prop_eval_depth_test (gint depth,
GError **error)
{
/* make sure we don't recurse too deep. in particular, guard against
* circular references.
*/
if (depth == 100)
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"maximal nesting level exceeded");
return FALSE;
}
return TRUE;
}
static gchar *
pika_prop_eval_read_token (const gchar **expr,
gchar **t,
GError **error)
{
const gchar *token;
g_free (*t);
*t = NULL;
/* skip whitespace */
while (g_ascii_isspace (**expr))
++*expr;
token = *expr;
if (*token == '\0')
return NULL;
/* name */
if (pika_prop_eval_is_name (token))
{
do { ++*expr; } while (g_ascii_isalnum (**expr) ||
**expr == '_' ||
**expr == '-');
}
/* string literal */
else if (token[0] == '\'')
{
for (++*expr; **expr != '\0' && **expr != '\''; ++*expr)
{
if (**expr == '\\')
{
++*expr;
if (**expr == '\0')
break;
}
}
if (**expr == '\0')
{
g_set_error (error, PIKA_PROP_EVAL_ERROR, PIKA_PROP_EVAL_FAILED,
"unterminated string literal");
return NULL;
}
++*expr;
}
/* punctuation or boolean literal */
else
{
++*expr;
}
*t = g_strndup (token, *expr - token);
return *t;
}
static gboolean
pika_prop_eval_is_name (const gchar *token)
{
return token && (g_ascii_isalpha (*token) || *token == '_');
}
static GQuark
pika_prop_eval_error_quark (void)
{
return g_quark_from_static_string ("pika-prop-eval-error-quark");
}