From 12476abdab83f765da8173e73b172ecf4749f6b9 Mon Sep 17 00:00:00 2001 From: katherine Date: Sat, 7 Dec 2019 15:16:24 -0700 Subject: initial commit --- LICENSE | 24 +++ README.md | 199 ++++++++++++++++++++++++ doc/example.c | 65 ++++++++ simple-xdg-bdirs.h | 438 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 726 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 doc/example.c create mode 100644 simple-xdg-bdirs.h diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cf1ab25 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +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. + +For more information, please refer to diff --git a/README.md b/README.md new file mode 100644 index 0000000..4173770 --- /dev/null +++ b/README.md @@ -0,0 +1,199 @@ +simple-xdg-bdirs +================ + +[simple-xdg-bdirs.h](simple-xdg-bdirs.h) is a single header file which +implements calculating file paths according to the +[XDG Base Directory Specification][1]. + +brief summary +------------- + +the XDG Base Directory Specification is a popular specification which defines +predictable locations for programs to store their config, runtime, cache, and +data files. these locations are calculated based on specially named environment +variables, with certain directories defined as default fallbacks. + +this single-header library provides a couple of simple functions to calculate +those locations at runtime, as well as functions to build file paths against +them. + +example +------- + +the following example file is available as [example.c](doc/example.c), which +you can compile and play with yourself. + +```C +#include +#include + +#include "../simple-xdg-bdirs.h" + +/* a simple example which attempts to read a value from a configuration file + * and write it back to a cache file */ +int main(int argc, char *argv[]) +{ + char *s, *wdir, **rdirs, **cur; + + /* alloc a null-terminated array of directories in which to search for + * configuration files */ + rdirs = simple_xdg_bdirs_read_dirs(SIMPLE_XDG_BDIRS_CONFIG); + + /* in case of error, errno is set, so perror works */ + if (rdirs == NULL) { + perror(NULL); + return 1; + } + + /* search in the config directories for the given relative path. if not + * found in the first, the second will be checked as a fallback etc */ + s = simple_xdg_bdirs_fullpath_read("xdg_bdirs_test/config.conf", rdirs); + + /* element strings of rdirs must also be freed */ + for (cur = rdirs; *cur != NULL; cur++) + free(*cur); + free(rdirs); + + /* check for errors again */ + if (s == NULL) { + perror(NULL); + return 1; + } + + /* print the fullpath of the file that was found */ + puts(s); + free(s); + + /* locate the directory into which runtime files should be written + * (things like sockets or lock files) */ + wdir = simple_xdg_bdirs_write_dir(SIMPLE_XDG_BDIRS_RUNTIME); + + if (wdir == NULL) { + perror(NULL); + return 1; + } + + /* a convenience function that builds a fullpath from a relative path and + * write directory */ + s = simple_xdg_bdirs_fullpath_write("xdg_bdirs_test.lock", wdir); + + free(wdir); + + if (s == NULL) { + perror(NULL); + return 1; + } + + puts(s); + free(s); + + return 0; +} +``` + +basically, each function returns either a string or `NULL`, in which case +`errno` will also be set. simple! + +interface +--------- + +```C +enum simple_xdg_bdirs_ftype { + SIMPLE_XDG_BDIRS_DATA, + SIMPLE_XDG_BDIRS_CONFIG, + SIMPLE_XDG_BDIRS_CACHE, + SIMPLE_XDG_BDIRS_RUNTIME, +}; +``` + +one of these types is passed to each of the functions below, denoting the +category of file to which the functions actions ought to correspond. + +- `SIMPLE_XDG_BDIRS_DATA` is used for persistent data created by a program +- `SIMPLE_XDG_BDIRS_CONFIG` is used for a program's configuration files +- `SIMPLE_XDG_BDIRS_CACHE` is used for transient data which may be deleted + without warning by the system +- `SIMPLE_XDG_BDIRS_RUNTIME` is used for "runtime files", such as lock files + or sockets + + +```C +static char* +simple_xdg_bdirs_write_dir(enum simple_xdg_bdirs_ftype type) +``` + +this function finds and returns a single string that is the path of a +directory into which all files of type `type` ought to be written. + +because the location of this directory is determined at runtime, and in +particular because it relies on the state of the filesystem and environment +variables, it is possible that no good candidate directory will be found, in +which case `NULL` will be returned. + +if a string is returned, it is guaranteed to always have a trailing '/', +meaning that a relative path can be appended directly without fear of +unintended results, like '/etx/xdgherbstluftwm' + +errors: +- `ENOENT`: no candidate dir found +- `EINVAL`: bad argument +- `EOVERFLOW`: extremely-large string encountered (unlikely) +- `ENOMEM`: malloc err + + +```C +static char** +simple_xdg_bdirs_read_dirs(enum simple_xdg_bdirs_ftype type) +``` + +this function finds and returns a null-terminated array of strings that are +the paths, in order, where one ought to search for files of type `type` when +reading. + +because some filetypes have only a single, environment-determined read dir, +it's possible that none will be found, in which case `NULL` will be +returned. + +returned strings are guaranteed to always have a trailing '/', meaning that +a relative path can be appended directly without fear of unintended results, +like '/etx/xdgherbstluftwm' + +errors: +- `ENOENT`: no candidate dirs found +- `EINVAL`: bad argument +- `EOVERFLOW`: extremely-large string encountered (unlikely) +- `ENOMEM`: malloc err + + +```C +static char* +simple_xdg_bdirs_fullpath_read(const char *rel_path, char **read_dirs) +``` + +take a relative path and set of directories returned from +`simple_xdg_bdirs_read_dirs` and return either a full path to an existing +file or, on error, NULL + +errors +- `ENOENT`: target file not found or readable +- `EINVAL`: bad argument +- `EOVERFLOW`: extremely-large string encountered (unlikely) +- `ENOMEM`: malloc err + + +```C +static char* +simple_xdg_bdirs_fullpath_write(const char *rel_path, char *write_dir) +``` + +take a relative path and a write directory returned from +`simple_xdg_bdirs_write_dir` and return either a full path to the targeted +write path or, on error, NULL + +errors +- `ENOENT`: write_dir is not found or not write accessible +- `EINVAL`: bad argument +- `EOVERFLOW`: extremely-large string encountered (unlikely) +- `ENOMEM`: malloc err + +[1]: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html diff --git a/doc/example.c b/doc/example.c new file mode 100644 index 0000000..688321a --- /dev/null +++ b/doc/example.c @@ -0,0 +1,65 @@ +#include +#include + +#include "../simple-xdg-bdirs.h" + +/* a simple example which attempts to read a value from a configuration file + * and write it back to a cache file */ +int main(int argc, char *argv[]) +{ + char *s, *wdir, **rdirs, **cur; + + /* alloc a null-terminated array of directories in which to search for + * configuration files */ + rdirs = simple_xdg_bdirs_read_dirs(SIMPLE_XDG_BDIRS_CONFIG); + + /* in case of error, errno is set, so perror works */ + if (rdirs == NULL) { + perror(NULL); + return 1; + } + + /* search in the config directories for the given relative path. if not + * found in the first, the second will be checked as a fallback etc */ + s = simple_xdg_bdirs_fullpath_read("xdg_bdirs_test/config.conf", rdirs); + + /* element strings of rdirs must also be freed */ + for (cur = rdirs; *cur != NULL; cur++) + free(*cur); + free(rdirs); + + /* check for errors again */ + if (s == NULL) { + perror(NULL); + return 1; + } + + /* print the fullpath of the file that was found */ + puts(s); + free(s); + + /* locate the directory into which runtime files should be written + * (things like sockets or lock files) */ + wdir = simple_xdg_bdirs_write_dir(SIMPLE_XDG_BDIRS_RUNTIME); + + if (wdir == NULL) { + perror(NULL); + return 1; + } + + /* a convenience function that builds a fullpath from a relative path and + * write directory */ + s = simple_xdg_bdirs_fullpath_write("xdg_bdirs_test.lock", wdir); + + free(wdir); + + if (s == NULL) { + perror(NULL); + return 1; + } + + puts(s); + free(s); + + return 0; +} diff --git a/simple-xdg-bdirs.h b/simple-xdg-bdirs.h new file mode 100644 index 0000000..98a57b5 --- /dev/null +++ b/simple-xdg-bdirs.h @@ -0,0 +1,438 @@ +#ifndef SIMPLE_XDG_BDIRS_H +#define SIMPLE_XDG_BDIRS_H + +#include +#include +#include +#include +#include +#include + +#include + +/* one of these types is passed to each of the functions below, denoting the + * category of file to which the functions actions ought to correspond. + * - `SIMPLE_XDG_BDIRS_DATA` is used for persistent data created by a program + * - `SIMPLE_XDG_BDIRS_CONFIG` is used for a program's configuration files + * - `SIMPLE_XDG_BDIRS_CACHE` is used for transient data which may be deleted + * without warning by the system + * - `SIMPLE_XDG_BDIRS_RUNTIME` is used for "runtime files", such as lock files + * or sockets + */ +enum simple_xdg_bdirs_ftype { + SIMPLE_XDG_BDIRS_DATA, + SIMPLE_XDG_BDIRS_CONFIG, + SIMPLE_XDG_BDIRS_CACHE, + SIMPLE_XDG_BDIRS_RUNTIME, +}; + +/* this function finds and returns a single string that is the path of a + * directory into which all files of type `type` ought to be written. + * + * because the location of this directory is determined at runtime, and in + * particular because it relies on the state of the filesystem and environment + * variables, it is possible that no good candidate directory will be found, in + * which case `NULL` will be returned. + * + * if a string is returned, it is guaranteed to always have a trailing '/', + * meaning that a relative path can be appended directly without fear of + * unintended results, like '/etx/xdgherbstluftwm' + * + * errors: + * ENOENT: no candidate dir found + * EINVAL: bad argument + * EOVERFLOW: extremely-large string encountered (unlikely) + * ENOMEM: malloc err + */ +static char* +simple_xdg_bdirs_write_dir(enum simple_xdg_bdirs_ftype type) +{ + char *home = getenv("HOME"); + char *xdg_data_home; + char *xdg_config_home; + char *xdg_cache_home; + char *xdg_runtime_dir; + char *empty = ""; + + char *s1, *s2, *r; + size_t l1, l2; + + /* get components */ + switch (type) { + case SIMPLE_XDG_BDIRS_DATA: + xdg_data_home = getenv("XDG_DATA_HOME"); + if (xdg_data_home && xdg_data_home[0]) { + s1 = empty; + s2 = xdg_data_home; + } else if (home && home[0]) { + s1 = home; + s2 = "/.local/share"; + } else { + errno = ENOENT; + return NULL; + } + break; + + case SIMPLE_XDG_BDIRS_CONFIG: + xdg_config_home = getenv("XDG_CONFIG_HOME"); + if (xdg_config_home && xdg_config_home[0]) { + s1 = empty; + s2 = xdg_config_home; + } else if (home && home[0]) { + s1 = home; + s2 = "/.config"; + } else { + errno = ENOENT; + return NULL; + } + break; + + case SIMPLE_XDG_BDIRS_CACHE: + xdg_cache_home = getenv("XDG_CACHE_HOME"); + if (xdg_cache_home && xdg_cache_home[0]) { + s1 = empty; + s2 = xdg_cache_home; + } else if (home && home[0]) { + s1 = home; + s2 = "/.cache"; + } else { + errno = ENOENT; + return NULL; + } + break; + + case SIMPLE_XDG_BDIRS_RUNTIME: + xdg_runtime_dir = getenv("XDG_RUNTIME_DIR"); + if (xdg_runtime_dir && xdg_runtime_dir[0]) { + s1 = empty; + s2 = xdg_runtime_dir; + } else { + errno = ENOENT; + return NULL; + } + break; + default: + errno = EINVAL; + return NULL; + } + + /* get s1 part length */ + for (l1 = 0; s1[l1]; l1++) { + if (l1 == SIZE_MAX) { + errno = EOVERFLOW; + return NULL; + } + } + + /* get s2 part length */ + for (l2 = 0; s2[l2]; l1++, l2++) { + if (l1 == SIZE_MAX) { + errno = EOVERFLOW; + return NULL; + } + } + + /* extra 2 for '/' and '\0' */ + for (l2 = 0; l2 < 2; l1++, l2++) { + if (l1 == SIZE_MAX) { + errno = EOVERFLOW; + return NULL; + } + } + + /* build str */ + r = malloc(l1 * sizeof(char)); + if (r == NULL) { + errno = ENOMEM; + return NULL; + } + + for (l1 = 0; s1[l1]; l1++) + r[l1] = s1[l1]; + + for (l2 = 0; s2[l2]; l1++, l2++) + r[l1] = s2[l2]; + + r[l1] = '/'; + + /* slash dedupe */ + for (l2 = 0; l2 < l1; l2++) { + if (r[l2] == '/' && r[l2 + 1] == '/') { + memmove(r + l2, r + l2 + 1, l1 - l2 - 1); + l1--; + l2--; + } + } + + r[l1 + 1] = '\0'; + + return r; +} + +/* this function finds and returns a null-terminated array of strings that are + * the paths, in order, where one ought to search for files of type `type` when + * reading. + * + * because some filetypes have only a single, environment-determined read dir, + * it's possible that none will be found, in which case `NULL` will be + * returned. + * + * returned strings are guaranteed to always have a trailing '/', meaning that + * a relative path can be appended directly without fear of unintended results, + * like '/etx/xdgherbstluftwm' + * + * errors: + * ENOENT: no candidate dirs found + * EINVAL: bad argument + * EOVERFLOW: extremely-large string encountered (unlikely) + * ENOMEM: malloc err + */ +static char** +simple_xdg_bdirs_read_dirs(enum simple_xdg_bdirs_ftype type) +{ + char *s1 = NULL, *s2 = NULL; + bool flag; + + size_t l1, l2, l3, l4; + + char **r = NULL, **cur; + + /* cache and runtime are the same as write_dir */ + switch (type) { + case SIMPLE_XDG_BDIRS_CACHE: + r = malloc(2 * sizeof(char*)); + + if (r == NULL) { + errno = ENOMEM; + return NULL; + } + + r[1] = NULL; + r[0] = simple_xdg_bdirs_write_dir(SIMPLE_XDG_BDIRS_CACHE); + + if (r[0] == NULL) + goto abort; + + return r; + + case SIMPLE_XDG_BDIRS_RUNTIME: + r = malloc(2 * sizeof(char*)); + + if (r == NULL) { + errno = ENOMEM; + return NULL; + } + + r[1] = NULL; + r[0] = simple_xdg_bdirs_write_dir(SIMPLE_XDG_BDIRS_RUNTIME); + + if (r[0] == NULL) + goto abort; + + return r; + + /* data and config are write_dir + fallbacks */ + case SIMPLE_XDG_BDIRS_DATA: + s1 = simple_xdg_bdirs_write_dir(SIMPLE_XDG_BDIRS_DATA); + + s2 = getenv("XDG_DATA_DIRS"); + if (!s2 || !s2[0]) + s2 = "/usr/local/share:/usr/share"; + + break; + + case SIMPLE_XDG_BDIRS_CONFIG: + s1 = simple_xdg_bdirs_write_dir(SIMPLE_XDG_BDIRS_CONFIG); + + s2 = getenv("XDG_CONFIG_DIRS"); + if (!s2 || !s2[0]) + s2 = "/etc/xdg"; + + break; + default: + errno = EINVAL; + return NULL; + } + + /* count number of fallback dirs */ + for (flag = true, l1 = 0, l2 = 1; s2[l1]; l1++) { + if (l1 == SIZE_MAX) { + errno = EOVERFLOW; + goto abort; + } + if (s2[l1] == ':') { + if (!flag) { + l2++; + flag = true; + } + } else { + flag = false; + } + } + + /* make r */ + r = malloc((l2 + (s1 != NULL) + 1) * sizeof(char*)); + if (r == NULL) { + errno = ENOMEM; + goto abort; + } + + /* insert write_dir, if it was found */ + if (s1 != NULL) { + r[0] = s1; + cur = r + 1; + } else { + cur = r; + } + + for (l1 = 0, l2 = 0; s2[l1];) { + /* skip over multiple ':' */ + for (; s2[l2] == ':'; l2++); + + /* check for done */ + l1 = l2; + if (s2[l1] == '\0') + break; + + /* find the end of the current dir in the string */ + for (; s2[l2] && s2[l2] != ':'; l2++); + + /* copy the current string into r */ + *cur = malloc((l2 - l1 + 2) * sizeof(char)); + if (*cur == NULL) { + errno = ENOMEM; + goto abort; + } + + strncpy(*cur, s2 + l1, l2 - l1); + (*cur)[l2 - l1] = '/'; + (*cur)[l2 - l1 + 1] = '\0'; + + /* slash dedupe */ + l3 = l2 - l1; + + for (l4 = 0; l4 < l3; l4++) { + if ((*cur)[l4] == '/' && (*cur)[l4 + 1] == '/') { + memmove(*cur + l4, *cur + l4 + 1, l3 - l4); + l3--; + l4--; + } + } + + (*cur)[l3 + 1] = '\0'; + + cur++; + } + + *cur = NULL; + + /* done ^_^ */ + return r; + +abort: + /* whoops, something went wrong :c */ + if (s1 != NULL) + free(s1); + + if (r == NULL) + return NULL; + + for (cur = r; *cur != NULL; cur++) + free(cur); + + free(r); + + return NULL; +} + +static char* +sub_simple_xdg_bdirs_join(const char *s1, const char *s2) +{ + size_t l1, l2, l3; + char *r; + + l1 = strlen(s1); + l2 = strlen(s2); + + for (l3 = l1; l3 != SIZE_MAX && l2 != 0; l3++, l2--); + + if (l3 == SIZE_MAX) { + errno = EOVERFLOW; + return NULL; + } + + r = malloc((l3 + 1) * sizeof(char)); + if (r == NULL) { + errno = ENOMEM; + return NULL; + } + + strcpy(r, s1); + strcpy(r + l1, s2); + + return r; +} + +/* take a relative path and set of directories returned from + * `simple_xdg_bdirs_read_dirs` and return either a full path to an existing + * file or, on error, NULL + * + * errors + * ENOENT: target file not found or readable + * EINVAL: bad argument + * EOVERFLOW: extremely-large string encountered (unlikely) + * ENOMEM: malloc err + */ +static char* +simple_xdg_bdirs_fullpath_read(const char *rel_path, char **read_dirs) +{ + char **cur, *path; + + if (rel_path == NULL && read_dirs == NULL) { + errno = EINVAL; + return NULL; + } + + for (cur = read_dirs; *cur != NULL; cur++) { + path = sub_simple_xdg_bdirs_join(*cur, rel_path); + if (path == NULL) + return NULL; + + if (!access(path, R_OK)) { + return path; + } + + free(path); + } + + errno = ENOENT; + return NULL; +} + +/* take a relative path and a write directory returned from + * `simple_xdg_bdirs_write_dir` and return either a full path to the targeted + * write path or, on error, NULL + * + * errors + * ENOENT: write_dir is not found or not write accessible + * EINVAL: bad argument + * EOVERFLOW: extremely-large string encountered (unlikely) + * ENOMEM: malloc err + */ +static char* +simple_xdg_bdirs_fullpath_write(const char *rel_path, char *write_dir) +{ + if (rel_path == NULL && write_dir == NULL) { + errno = EINVAL; + return NULL; + } + + if (access(write_dir, W_OK)) { + errno = ENOENT; + return NULL; + } + + return sub_simple_xdg_bdirs_join(write_dir, rel_path); +} + +#endif -- cgit v1.2.3