xenocara/lib/xcb-util-xrm/src/database.c
jasper 44c5a3cdf0 add xcb-util-xrm-1.0
ok matthieu@
2016-11-07 19:20:35 +00:00

516 lines
16 KiB
C

/*
* vim:ts=4:sw=4:expandtab
*
* Copyright © 2016 Ingo Bürk
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* Except as contained in this notice, the names of the authors or their
* institutions shall not be used in advertising or otherwise to promote the
* sale, use or other dealings in this Software without prior written
* authorization from the authors.
*
*/
#include "externals.h"
#include "database.h"
#include "match.h"
#include "util.h"
#ifndef MAX_INCLUDE_DEPTH
/* We want to limit the maximum depth of (recursive) #include directives. This
* is to avoid accidental cyclic inclusions which would lead to an endless loop
* otherwise. */
#define MAX_INCLUDE_DEPTH 100
#endif
/* Forward declarations */
static xcb_xrm_database_t *__xcb_xrm_database_from_string(const char *_str, const char *base, int depth);
static xcb_xrm_database_t *__xcb_xrm_database_from_file(const char *_filename, const char *base, int depth);
static void __xcb_xrm_database_put(xcb_xrm_database_t *database, xcb_xrm_entry_t *entry, bool override);
/*
* Creates a database similarly to XGetDefault(). For typical applications,
* this is the recommended way to construct the resource database.
*
* The database is created as follows:
* - If the RESOURCE_MANAGER property exists on the root window of
* screen 0, the database is constructed from it using @ref
* xcb_xrm_database_from_resource_manager().
* - Otherwise, if $HOME/.Xresources exists, the database is constructed from
* it using @ref xcb_xrm_database_from_file().
* - Otherwise, if $HOME/.Xdefaults exists, the database is constructed from
* it using @ref xcb_xrm_database_from_file().
* - If the environment variable XENVIRONMENT is set, the file specified by
* it is loaded using @ref xcb_xrm_database_from_file and then combined with
* the database using @ref xcb_xrm_database_combine() with override set to
* true.
* If XENVIRONMENT is not specified, the same is done with
* $HOME/.Xdefaults-$HOSTNAME, wherein $HOSTNAME is determined by
* gethostname(2).
*
* This represents the way XGetDefault() creates the database for the most
* part, but is not exactly the same. In particular, XGetDefault() does not
* consider $HOME/.Xresources.
*
* @param conn XCB connection.
* @returns The constructed database. Can return NULL, e.g., if the screen
* cannot be determined.
*/
xcb_xrm_database_t *xcb_xrm_database_from_default(xcb_connection_t *conn) {
xcb_screen_t *screen;
xcb_xrm_database_t *database;
char *xenvironment;
screen = xcb_aux_get_screen(conn, 0);
if (screen == NULL)
return NULL;
/* 1. Try to load the database from RESOURCE_MANAGER. */
database = xcb_xrm_database_from_resource_manager(conn, screen);
/* 2. Otherwise, try to load the database from $HOME/.Xresources. */
if (database == NULL) {
char *xresources = get_home_dir_file(".Xresources");
database = xcb_xrm_database_from_file(xresources);
FREE(xresources);
}
/* 3. Otherwise, try to load the database from $HOME/.Xdefaults. */
if (database == NULL) {
char *xdefaults = get_home_dir_file(".Xdefaults");
database = xcb_xrm_database_from_file(xdefaults);
FREE(xdefaults);
}
/* 4. If XENVIRONMENT is specified, merge the database defined by that file.
* Otherwise, use $HOME/.Xdefaults-$HOSTNAME. */
if ((xenvironment = getenv("XENVIRONMENT")) != NULL) {
xcb_xrm_database_t *source = xcb_xrm_database_from_file(xenvironment);
xcb_xrm_database_combine(source, &database, true);
xcb_xrm_database_free(source);
} else {
char hostname[1024];
hostname[1023] = '\0';
if (gethostname(hostname, 1023) == 0) {
char *name;
if (asprintf(&name, ".Xdefaults-%s", hostname) >= 0) {
xcb_xrm_database_t *source;
char *xdefaults = get_home_dir_file(name);
FREE(name);
source = xcb_xrm_database_from_file(xdefaults);
FREE(xdefaults);
xcb_xrm_database_combine(source, &database, true);
xcb_xrm_database_free(source);
}
}
}
return database;
}
/*
* Loads the RESOURCE_MANAGER property and creates a database with its
* contents. If the database could not be created, this function will return
* NULL.
*
* @param conn A working XCB connection.
* @param screen The xcb_screen_t* screen to use.
* @returns The database described by the RESOURCE_MANAGER property.
*
* @ingroup xcb_xrm_database_t
*/
xcb_xrm_database_t *xcb_xrm_database_from_resource_manager(xcb_connection_t *conn, xcb_screen_t *screen) {
xcb_xrm_database_t *database;
char *resources = xcb_util_get_property(conn, screen->root, XCB_ATOM_RESOURCE_MANAGER,
XCB_ATOM_STRING, 16 * 1024);
if (resources == NULL) {
return NULL;
}
/* Parse the resource string. */
database = xcb_xrm_database_from_string(resources);
FREE(resources);
return database;
}
/*
* Creates a database from the given string.
* If the database could not be created, this function will return NULL.
*
* @param str The resource string.
* @returns The database described by the resource string.
*
* @ingroup xcb_xrm_database_t
*/
xcb_xrm_database_t *xcb_xrm_database_from_string(const char *str) {
return __xcb_xrm_database_from_string(str, NULL, 0);
}
static xcb_xrm_database_t *__xcb_xrm_database_from_string(const char *_str, const char *base, int depth) {
xcb_xrm_database_t *database;
char *str;
int num_continuations = 0;
char *str_continued;
char *outwalk;
char *saveptr = NULL;
if (_str == NULL)
return xcb_xrm_database_from_string("");
str = strdup(_str);
if (str == NULL)
return NULL;
/* Count the number of line continuations. */
for (char *walk = str; *walk != '\0'; walk++) {
if (*walk == '\\' && *(walk + 1) == '\n') {
num_continuations++;
}
}
/* Take care of line continuations. */
str_continued = calloc(1, strlen(str) + 1 - 2 * num_continuations);
if (str_continued == NULL) {
FREE(str);
return NULL;
}
outwalk = str_continued;
for (char *walk = str; *walk != '\0'; walk++) {
if (*walk == '\\' && *(walk + 1) == '\n') {
walk++;
continue;
}
*(outwalk++) = *walk;
}
*outwalk = '\0';
database = calloc(1, sizeof(struct xcb_xrm_database_t));
if (database == NULL) {
FREE(str);
FREE(str_continued);
return NULL;
}
TAILQ_INIT(database);
for (char *line = strtok_r(str_continued, "\n", &saveptr); line != NULL; line = strtok_r(NULL, "\n", &saveptr)) {
/* Handle include directives. */
if (line[0] == '#') {
int i = 1;
/* Skip whitespace and quotes. */
while (line[i] == ' ' || line[i] == '\t')
i++;
if (depth < MAX_INCLUDE_DEPTH &&
line[i++] == 'i' &&
line[i++] == 'n' &&
line[i++] == 'c' &&
line[i++] == 'l' &&
line[i++] == 'u' &&
line[i++] == 'd' &&
line[i++] == 'e') {
xcb_xrm_database_t *included;
char *filename;
char *copy;
char *new_base;
int j = strlen(line) - 1;
/* Skip whitespace and quotes. */
while (line[i] == ' ' || line[i] == '\t' || line[i] == '"')
i++;
while (line[j] == ' ' || line[j] == '\t' || line[j] == '"')
j--;
if (j < i) {
/* Only whitespace left in this line. */
continue;
}
line[j+1] = '\0';
filename = resolve_path(&line[i], base);
if (filename == NULL)
continue;
/* We need to strdup() the filename since dirname() will modify it. */
copy = strdup(filename);
if (copy == NULL) {
FREE(filename);
continue;
}
new_base = dirname(copy);
if (new_base == NULL) {
FREE(filename);
FREE(copy);
continue;
}
included = __xcb_xrm_database_from_file(filename, new_base, depth + 1);
FREE(filename);
FREE(copy);
if (included != NULL) {
xcb_xrm_database_combine(included, &database, true);
xcb_xrm_database_free(included);
}
continue;
}
}
xcb_xrm_database_put_resource_line(&database, line);
}
FREE(str);
FREE(str_continued);
return database;
}
/*
* Creates a database from a given file.
* If the file cannot be found or opened, NULL is returned.
*
* @param filename Valid filename.
* @returns The database described by the file's contents.
*/
xcb_xrm_database_t *xcb_xrm_database_from_file(const char *filename) {
return __xcb_xrm_database_from_file(filename, NULL, 0);
}
static xcb_xrm_database_t *__xcb_xrm_database_from_file(const char *_filename, const char *base, int depth) {
char *filename = NULL;
char *copy = NULL;
char *new_base = NULL;
char *content = NULL;
xcb_xrm_database_t *database = NULL;
if (_filename == NULL)
return NULL;
filename = resolve_path(_filename, base);
if (filename == NULL)
return NULL;
/* We need to strdup() the filename since dirname() will modify it. */
copy = strdup(filename);
if (copy == NULL)
goto done_from_file;
new_base = dirname(copy);
if (new_base == NULL)
goto done_from_file;
content = file_get_contents(filename);
if (content == NULL)
goto done_from_file;
database = __xcb_xrm_database_from_string(content, new_base, depth);
done_from_file:
FREE(filename);
FREE(copy);
FREE(content);
return database;
}
/*
* Returns a string representation of a database.
* The string is owned by the caller and must be free'd.
*
* @param database The database to return in string format.
* @returns A string representation of the specified database.
*/
char *xcb_xrm_database_to_string(xcb_xrm_database_t *database) {
char *result = NULL;
xcb_xrm_entry_t *entry;
if (database == NULL)
return NULL;
TAILQ_FOREACH(entry, database, entries) {
char *entry_str = __xcb_xrm_entry_to_string(entry);
char *tmp;
if (asprintf(&tmp, "%s%s\n", result == NULL ? "" : result, entry_str) < 0) {
FREE(entry_str);
FREE(result);
return NULL;
}
FREE(entry_str);
FREE(result);
result = tmp;
}
return result;
}
/*
* Combines two databases.
* The entries from the source database are stored in the target database. If
* the same specifier already exists in the target database, the value will be
* overridden if override is set; otherwise, the value is discarded.
* If NULL is passed for target_db, a new and empty database will be created
* and returned in the pointer.
*
* @param source_db Source database.
* @param target_db Target database.
* @param override If true, entries from the source database override entries
* in the target database using the same resource specifier.
*/
void xcb_xrm_database_combine(xcb_xrm_database_t *source_db, xcb_xrm_database_t **target_db, bool override) {
xcb_xrm_entry_t *entry;
if (*target_db == NULL)
*target_db = xcb_xrm_database_from_string("");
if (source_db == NULL)
return;
if (source_db == *target_db)
return;
TAILQ_FOREACH(entry, source_db, entries) {
xcb_xrm_entry_t *copy = __xcb_xrm_entry_copy(entry);
__xcb_xrm_database_put(*target_db, copy, override);
}
}
/*
* Inserts a new resource into the database.
* If the resource already exists, the current value will be replaced.
* If NULL is passed for database, a new and empty database will be created and
* returned in the pointer.
*
* Note that this is not the equivalent of @ref
* xcb_xrm_database_put_resource_line when concatenating the resource name and
* value with a colon. For example, if the value starts with a leading space,
* this must (and will) be replaced with the special '\ ' sequence.
*
* @param database The database to modify.
* @param resource The fully qualified or partial resource specifier.
* @param value The value of the resource.
*/
void xcb_xrm_database_put_resource(xcb_xrm_database_t **database, const char *resource, const char *value) {
char *escaped;
char *line;
assert(resource != NULL);
assert(value != NULL);
if (*database == NULL)
*database = xcb_xrm_database_from_string("");
escaped = __xcb_xrm_entry_escape_value(value);
if (escaped == NULL)
return;
if (asprintf(&line, "%s: %s", resource, escaped) < 0) {
FREE(escaped);
return;
}
FREE(escaped);
xcb_xrm_database_put_resource_line(database, line);
FREE(line);
}
/*
* Inserts a new resource into the database.
* If the resource already exists, the current value will be replaced.
* If NULL is passed for database, a new and empty database will be created and
* returned in the pointer.
*
* @param database The database to modify.
* @param line The complete resource specification to insert.
*/
void xcb_xrm_database_put_resource_line(xcb_xrm_database_t **database, const char *line) {
xcb_xrm_entry_t *entry;
assert(line != NULL);
if (*database == NULL)
*database = xcb_xrm_database_from_string("");
/* Ignore comments and directives. The specification guarantees that no
* whitespace is allowed before these characters. */
if (line[0] == '!' || line[0] == '#')
return;
if (xcb_xrm_entry_parse(line, &entry, false) == 0) {
__xcb_xrm_database_put(*database, entry, true);
}
}
/**
* Destroys the given database.
*
* @param database The database to destroy.
*
* @ingroup xcb_xrm_database_t
*/
void xcb_xrm_database_free(xcb_xrm_database_t *database) {
if (database == NULL)
return;
while (!TAILQ_EMPTY(database)) {
xcb_xrm_entry_t *entry = TAILQ_FIRST(database);
TAILQ_REMOVE(database, entry, entries);
xcb_xrm_entry_free(entry);
}
FREE(database);
}
static void __xcb_xrm_database_put(xcb_xrm_database_t *database, xcb_xrm_entry_t *entry, bool override) {
xcb_xrm_entry_t *current;
if (database == NULL || entry == NULL)
return;
/* Let's see whether this is a duplicate entry. */
current = TAILQ_FIRST(database);
while (current != NULL) {
xcb_xrm_entry_t *previous = TAILQ_PREV(current, xcb_xrm_database_t, entries);
if (__xcb_xrm_entry_compare(entry, current) == 0) {
if (!override) {
xcb_xrm_entry_free(entry);
return;
}
TAILQ_REMOVE(database, current, entries);
xcb_xrm_entry_free(current);
current = previous;
if (current == NULL)
current = TAILQ_FIRST(database);
}
if (current == NULL)
break;
current = TAILQ_NEXT(current, entries);
}
TAILQ_INSERT_TAIL(database, entry, entries);
}