#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 inline 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 inline 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