diff options
Diffstat (limited to 'src/paths.c')
| -rw-r--r-- | src/paths.c | 1030 |
1 files changed, 1030 insertions, 0 deletions
diff --git a/src/paths.c b/src/paths.c new file mode 100644 index 0000000..6a253ca --- /dev/null +++ b/src/paths.c @@ -0,0 +1,1030 @@ +#include "common.h" + +#include "paths.h" +#include "ref.h" +#include "thread.h" +#include "safe_fifo.h" +#include "strutil.h" + +#include <sys/file.h> +#include <sys/stat.h> +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <glob.h> +#include <pwd.h> +#include <stdarg.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#define SEARCH_CACHE_SIZE (2) + +typedef struct search_t +{ + const char* dir; + const char** dirs; + char* match; + const char* match_name; + bool plain; + paths_find_callback_t find_callback; + void* userdata; +} search_t; + +typedef struct searchargs_t +{ + bool quit; + safe_fifo_t* queue; + thread_lock_t* lock; + search_t* cache[SEARCH_CACHE_SIZE]; +} searchargs_t; + +struct paths_t +{ + REF_DECLARE(); + char* user_data; + char* user_config; + char* user_cache; + char* user_runtime; + char** search_data; + char** search_config; + + searchargs_t* search; +#ifdef DEBUG + thread_t* searcher; +#endif +}; + +static void free_list(char** list); +static void free_search_data(search_t* search); +static bool paths_init(paths_t* paths); +static char* build_path(paths_t* paths, paths_source_t source, ...); +static bool create_dir(paths_t* paths, paths_source_t source, + const char* addon, size_t addon_len); +static void* searcher(void* _args); +static search_t* get_search(searchargs_t* args); +static void put_search(searchargs_t* args, search_t* search); + +paths_t* paths_new(void) +{ + paths_t* paths = calloc(1, sizeof(paths_t)); + if (!paths) + { + return NULL; + } + if (!REF_INIT(paths)) + { + free(paths); + return NULL; + } + for (;;) + { + paths->search = calloc(1, sizeof(searchargs_t)); + if (!paths->search) + { + break; + } + paths->search->lock = thread_lock_new(); + if (!paths->search->lock) + { + break; + } + paths->search->queue = safe_fifo_new(); + if (!paths->search->queue) + { + break; + } + safe_fifo_ref(paths->search->queue); +#ifdef DEBUG + paths->searcher = thread_joinable_new(searcher, paths->search); + if (!paths->searcher) + { + safe_fifo_unref(paths->search->queue); + break; + } +#else + if (!thread_new(searcher, paths->search)) + { + safe_fifo_unref(paths->search->queue); + break; + } +#endif + if (!paths_init(paths)) + { + break; + } + return paths; + } + paths_unref(paths); + return NULL; +} + +void paths_ref(paths_t* paths) +{ + assert(paths); + REF_INC(paths); +} + +void paths_unref(paths_t* paths) +{ + if (paths && REF_DEC(paths)) + { + if (paths->search) + { + if (paths->search->queue) + { + search_t* search = get_search(paths->search); + paths->search->quit = true; + if (search) + { + memset(search, 0, sizeof(search_t)); + safe_fifo_push(paths->search->queue, search); + } + safe_fifo_unref(paths->search->queue); + } +#ifdef DEBUG + thread_join(paths->searcher); +#endif + } + free(paths->user_data); + free(paths->user_config); + free(paths->user_cache); + free(paths->user_runtime); + free_list(paths->search_data); + free_list(paths->search_config); + REF_FREE(paths); + free(paths); + } +} + +const char* paths_user_dir(paths_t* paths, paths_source_t source) +{ + assert(paths); + switch (source) + { + case PATHS_DATA: + return paths->user_data; + case PATHS_CONFIG: + return paths->user_config; + case PATHS_CACHE: + return paths->user_cache; + case PATHS_RUNTIME: + return paths->user_runtime; + } + assert(false); + return NULL; +} + +const char** paths_search_dirs(paths_t* paths, paths_source_t source) +{ + assert(paths); + switch (source) + { + case PATHS_DATA: + return (const char**)paths->search_data; + case PATHS_CONFIG: + return (const char**)paths->search_config; + case PATHS_CACHE: + case PATHS_RUNTIME: + return NULL; + } + assert(false); + return NULL; +} + +struct paths_file_t +{ + paths_source_t source; + char* path, * target; + int fd; +}; + +paths_file_t* paths_write(paths_t* paths, paths_source_t source, + const char* filename, unsigned int flags, ...) +{ + paths_file_t* file; + int f; + const char* subpath; + assert(filename); + if (source == PATHS_RUNTIME && !paths->user_runtime) + { + errno = ENOENT; + return NULL; + } + if (filename[0] == '/') + { + filename++; + } + subpath = strrchr(filename, '/'); + if (subpath) + { + if (!create_dir(paths, source, filename, subpath - filename)) + { + return NULL; + } + } + else + { + if (!create_dir(paths, source, NULL, 0)) + { + return NULL; + } + } + file = calloc(1, sizeof(paths_file_t)); + if (!file) + { + return NULL; + } + file->source = source; + if (source == PATHS_CONFIG && + (((flags & PATHS_CREATE) && (flags & PATHS_TRUNCATE)) || + ((flags & PATHS_CREATE) && (flags & PATHS_EXCLUSIVE)))) + { + file->target = build_path(paths, source, filename, NULL); + if (!file->target) + { + free(file); + return NULL; + } + file->path = build_path(paths, source, ".#", filename, NULL); + } + else + { + file->path = build_path(paths, source, filename, NULL); + } + if (!file->path) + { + free(file->target); + free(file); + return NULL; + } + f = O_WRONLY; + if ((flags & PATHS_APPEND)) + { + f |= O_APPEND; + } + if ((flags & PATHS_CREATE)) + { + f |= O_CREAT; + if ((flags & PATHS_EXCLUSIVE)) + { + f |= O_EXCL; + } + } + if ((flags & PATHS_TRUNCATE)) + { + f |= O_TRUNC; + } +#ifdef O_EXLOCK + f |= O_EXLOCK; +#endif + if ((f & O_CREAT)) + { + va_list args; + va_start(args, flags); + file->fd = open(file->path, f, va_arg(args, int)); + va_end(args); + } + else + { + file->fd = open(file->path, f); + } + if (file->fd < 0) + { + free(file->path); + free(file->target); + free(file); + return NULL; + } +#ifndef O_EXLOCK + for (;;) + { + if (flock(file->fd, LOCK_EX) == 0) + { + break; + } + if (errno != EINTR) + { + if ((flags & O_CREAT)) + { + unlink(file->path); + } + close(file->fd); + free(file->path); + free(file->target); + free(file); + return NULL; + } + } +#endif + return file; +} + +ssize_t paths_file_write(paths_file_t* file, + const void* data, size_t size) +{ + size_t pos = 0; + assert(file); + assert(size == 0 || data); + while (pos < size) + { + ssize_t ret = write(file->fd, data + pos, size - pos); + if (ret < 0) + { + if (errno == EAGAIN || errno == EWOULDBLOCK || + errno == EINTR) + { + /* Non fatal errors */ + if (pos > 0) + { + return pos; + } + } + return -1; + } + if (ret == 0) + { + return pos; + } + pos += ret; + } + return size; +} + +int paths_file_close(paths_file_t* file) +{ + assert(file); + if (close(file->fd)) + { + return -1; + } + file->fd = -1; + if (file->target) + { + size_t len = strlen(file->target); + char* tmp = malloc(len + 2); + int ret; + if (!tmp) + { + return -1; + } + memcpy(tmp, file->target, len); + memcpy(tmp + len, "~", 2); + ret = rename(file->target, tmp); + free(tmp); + if (ret && errno == ENOENT) + { + int olderrno = errno; + /* Check if rename failed because target doesn't exist */ + if (access(file->target, F_OK) == 0 || + errno != ENOENT) + { + /* Unable to create backup */ + errno = olderrno; + return -1; + } + } + if (rename(file->path, file->target)) + { + return -1; + } + free(file->target); + } + free(file->path); + free(file); + return 0; +} + +void paths_file_abort(paths_file_t* file) +{ + if (file) + { + if (file->fd >= 0) + { + close(file->fd); + } + unlink(file->path); + free(file->path); + free(file->target); + free(file); + } +} + +bool create_dir(paths_t* paths, paths_source_t source, const char* addon, + size_t addon_len) +{ + const char* dir = paths_user_dir(paths, source); + char* tmp = NULL; + assert(dir); + assert(addon || !addon_len); + if (addon_len) + { + size_t dlen = strlen(dir); + tmp = malloc(dlen + 1 + addon_len + 1); + if (!tmp) + { + return false; + } + memcpy(tmp, dir, dlen); + tmp[dlen++] = '/'; + memcpy(tmp + dlen, addon, addon_len); + tmp[dlen + addon_len] = '\0'; + dir = tmp; + } + if (mkdir(dir, 0700)) + { + free(tmp); + return errno == EEXIST; + } + free(tmp); + return true; +} + +void free_list(char** list) +{ + if (list) + { + char** i; + for (i = list; *i; i++) + { + free(*i); + } + free(list); + } +} + +typedef struct homedir_t +{ + char* path; + size_t len; +} homedir_t; + +static char* get_userdir(const char* envvar, homedir_t* homedir, + const char* addon); +static char** get_searchdirs(const char* envvar, const char* default_list); + +static bool valid_runtime(const char* path); + +bool paths_init(paths_t* paths) +{ + homedir_t homedir = { NULL, 0 }; + paths->user_data = get_userdir("XDG_DATA_HOME", &homedir, "/.local/share"); + paths->user_config = get_userdir("XDG_CONFIG_HOME", &homedir, "/.config"); + paths->user_cache = get_userdir("XDG_CACHE_HOME", &homedir, "/.cache"); + paths->user_runtime = get_userdir("XDG_RUNTIME_HOME", NULL, NULL); + free(homedir.path); + if (!paths->user_data || !paths->user_config || !paths->user_cache) + { + return false; + } + if (paths->user_runtime) + { + if (!valid_runtime(paths->user_runtime)) + { + free(paths->user_runtime); + paths->user_runtime = NULL; + } + } + paths->search_data = get_searchdirs("XDG_DATA_DIRS", + "/usr/local/share:/usr/share"); + if (!paths->search_data) + { + return false; + } + paths->search_config = get_searchdirs("XDG_CONFIG_DIRS", "/etc/xdg"); + if (!paths->search_config) + { + return false; + } + return true; +} + +static void cleanup_dir(char* path, size_t len) +{ + while (len > 1 && path[len - 1] == '/') + { + path[--len] = '\0'; + } +} + +static char* get_homedir(void) +{ + const char* data = getenv("HOME"); + size_t len; + char* ret; + if (data && *data) + { + len = strlen(data); + ret = malloc(len + 1); + if (ret) + { + memcpy(ret, data, len + 1); + cleanup_dir(ret, len); + } + return ret; + } + for (;;) + { + long size = sysconf(_SC_GETPW_R_SIZE_MAX); + char* buf; + struct passwd entry, *result = NULL; + assert(size != -1); + if (size == -1) + { + break; + } + buf = malloc(size); + if (!buf) + { + break; + } + if (getpwuid_r(getuid(), &entry, buf, size, &result) != 0) + { + /* Error getting entry */ + free(buf); + break; + } + if (!result) + { + /* No entry found */ + free(buf); + break; + } + len = strlen(entry.pw_dir); + if (len == 0) + { + free(buf); + break; + } + ret = malloc(len + 1); + if (!ret) + { + free(buf); + return NULL; + } + memcpy(ret, entry.pw_dir, len + 1); + free(buf); + cleanup_dir(ret, len); + return ret; + } + /* When all else fails */ + return strdup("/"); +} + +char* get_userdir(const char* envvar, homedir_t* homedir, const char* addon) +{ + const char* data = getenv(envvar); + size_t len; + char* ret; + assert(!addon || addon[0] == '/'); + if (data && *data) + { + len = strlen(data); + ret = malloc(len + 1); + if (ret) + { + memcpy(ret, data, len + 1); + cleanup_dir(ret, len); + } + return ret; + } + if (!homedir && !addon) + { + return NULL; + } + assert(homedir && addon); + if (!homedir->path) + { + homedir->path = get_homedir(); + if (!homedir->path) + { + return NULL; + } + homedir->len = strlen(homedir->path); + } + len = homedir->len + strlen(addon); + ret = malloc(len + 1); + if (ret) + { + memcpy(ret, homedir->path, homedir->len); + if (homedir->len > 1) + { + assert(homedir->path[homedir->len - 1] != '/'); + memcpy(ret + homedir->len, addon, (len - homedir->len) + 1); + } + else + { + assert(homedir->path[0] == '/'); + len--; + memcpy(ret + homedir->len, addon + 1, len - homedir->len); + ret[len] = '\0'; + } + } + return ret; +} + +static void cleanup_dirs(char** dirs) +{ + char** d; + for (d = dirs; *d; d++) + { + cleanup_dir(*d, strlen(*d)); + } +} + +static char** splitlist(const char* str) +{ + size_t count = 0, size = 2; + char** ret = calloc(size + 1, sizeof(char*)); + const char* last = str, * pos; + if (!ret) + { + return NULL; + } + for (;;) + { + size_t len; + pos = strchr(last, ':'); + if (!pos) + { + pos = last + strlen(last); + } + + if (count == size) + { + size_t ns = size * 2; + char** tmp = realloc(ret, (ns + 1) * sizeof(char*)); + if (!tmp) + { + free_list(ret); + return NULL; + } + memset(tmp + size, 0, (ns + 1 - size) * sizeof(char*)); + ret = tmp; + size = ns; + } + len = pos - last; + ret[count] = strdup_len(last, len); + if (!ret[count]) + { + free_list(ret); + return NULL; + } + count++; + + if (*pos) + { + last = pos + 1; + } + else + { + break; + } + } + assert(!ret[count]); + return ret; +} + +char** get_searchdirs(const char* envvar, const char* default_list) +{ + const char* data = getenv(envvar); + char** ret; + bool cleanup; + if (data && *data) + { + ret = splitlist(data); + cleanup = true; + } + else + { + ret = splitlist(default_list); + cleanup = false; + } + if (!ret) + { + return NULL; + } + if (cleanup) + { + cleanup_dirs(ret); + } + return ret; +} + +bool valid_runtime(const char* path) +{ + struct stat buf; + if (stat(path, &buf) != 0) + { + return false; + } + if (buf.st_uid != getuid()) + { + return false; + } + if ((buf.st_mode & 0777) != 0700) + { + return false; + } + return true; +} + +char* build_path(paths_t* paths, paths_source_t source, ...) +{ + va_list args; + bool first = true; + dynstr_t* ret; + const char* base = paths_user_dir(paths, source), * part; + assert(base); + ret = dynstr_new_str(base); + if (!ret) + { + return NULL; + } + va_start(args, source); + while ((part = va_arg(args, const char*))) + { + if (first) + { + dynstr_appendc(ret, '/'); + first = false; + } + dynstr_append(ret, part); + } + return dynstr_done(ret); +} + +static bool need_glob(char* match) +{ + const char* magic_chars = "*?[]{}"; + char* s; + bool need_unescape = false; + for (s = match; *s; s++) + { + if (*s == '\\') + { + s++; + need_unescape = true; + } + else if (strchr(magic_chars, *s)) + { + return true; + } + } + if (need_unescape) + { + unescape(match); + } + return false; +} + +void paths_find(paths_t* paths, paths_source_t source, + const char* match, paths_find_callback_t find_callback, + void* userdata) +{ + search_t* search; + assert(paths && match && find_callback); + search = get_search(paths->search); + search->dir = paths_user_dir(paths, source); + search->dirs = paths_search_dirs(paths, source); + search->match = strdup(match); + search->find_callback = find_callback; + search->userdata = userdata; + if (!search->match) + { + find_callback(userdata, NULL); + put_search(paths->search, search); + return; + } + if (*search->match == '/') + { + size_t len = strlen(search->match); + memmove(search->match, search->match + 1, len); + } + if (!*search->match) + { + find_callback(userdata, NULL); + put_search(paths->search, search); + return; + } + search->plain = !need_glob(search->match); + if (search->plain) + { + search->match_name = strrchr(search->match, '/'); + if (search->match_name) + { + search->match_name++; + } + else + { + search->match_name = search->match; + } + } + safe_fifo_push(paths->search->queue, search); +} + +search_t* get_search(searchargs_t* args) +{ + size_t i; + thread_lock_lock(args->lock); + for (i = 0; i < SEARCH_CACHE_SIZE; i++) + { + if (args->cache[i]) + { + search_t* ret = args->cache[i]; + args->cache[i] = NULL; + thread_lock_unlock(args->lock); + return ret; + } + } + thread_lock_unlock(args->lock); + return malloc(sizeof(search_t)); +} + +void put_search(searchargs_t* args, search_t* search) +{ + size_t i; + thread_lock_lock(args->lock); + for (i = 0; i < SEARCH_CACHE_SIZE; i++) + { + if (!args->cache[i]) + { + args->cache[i] = search; + free_search_data(search); + thread_lock_unlock(args->lock); + return; + } + } + thread_lock_unlock(args->lock); + free_search_data(search); + free(search); +} + +void free_search_data(search_t* search) +{ + free(search->match); +} + +static bool search_dir(search_t* search, const char* dir) +{ + bool canceled = false; + if (search->plain) + { + DIR* dh; + struct dirent* d; + char* path = NULL; + size_t dirlen = strlen(dir); + size_t matchlen = strlen(search->match); + if (search->match == search->match_name) + { + path = malloc(dirlen + 1 + matchlen + 1); + if (!path) + { + return false; + } + memcpy(path, dir, dirlen + 1); + } + else + { + size_t subdirlen = search->match_name - 1 - search->match; + matchlen = strlen(search->match_name); + path = malloc(dirlen + 1 + subdirlen + 1 + matchlen + 1); + assert(search->match_name > search->match); + assert(search->match_name[-1] == '/'); + if (!path) + { + return false; + } + memcpy(path, dir, dirlen); + path[dirlen++] = '/'; + memcpy(path + dirlen, search->match, subdirlen); + dirlen += subdirlen; + path[dirlen] = '\0'; + } + dh = opendir(path); + if (!dh) + { + free(path); + return false; + } + path[dirlen] = '/'; + while ((d = readdir(dh))) + { + if (strcmp(d->d_name, search->match_name) == 0) + { + memcpy(path + dirlen + 1, search->match_name, matchlen + 1); + if (!search->find_callback(search->userdata, path)) + { + canceled = true; + break; + } + } + } + closedir(dh); + free(path); + } + else + { + glob_t data; + dynstr_t* str; + char* pattern; + str = dynstr_new_str(dir); + if (!str) + { + return false; + } + if (!dynstr_escape(str, "*?[]{}") || !dynstr_appendc(str, '/') || + !dynstr_append(str, search->match)) + { + dynstr_free(str); + return false; + } + pattern = dynstr_done(str); + memset(&data, 0, sizeof(glob_t)); + if (glob(pattern, GLOB_NOSORT, NULL, &data) == 0) + { + size_t i; + for (i = 0; i < data.gl_pathc; i++) + { + if (!search->find_callback(search->userdata, data.gl_pathv[i])) + { + canceled = true; + break; + } + } + globfree(&data); + } + free(pattern); + } + return canceled; +} + +void* searcher(void* _args) +{ + searchargs_t* args = _args; + size_t i; + + while (!args->quit) + { + search_t* search = safe_fifo_pop(args->queue); + bool canceled = false; + if (!search->match) + { + free(search); + assert(args->quit); + break; + } + do + { + if (search->dir) + { + if (args->quit || + (canceled = search_dir(search, search->dir))) + { + break; + } + } + if (search->dirs) + { + const char** dir; + for (dir = search->dirs; *dir; dir++) + { + if (args->quit || + (canceled = search_dir(search, *dir))) + { + break; + } + } + } + } while (false); + if (!canceled) + { + search->find_callback(search->userdata, NULL); + } + put_search(args, search); + } + + /* Free all items in the queue */ + for (;;) + { + search_t* search = safe_fifo_trypop(args->queue); + if (!search) + { + break; + } + put_search(args, search); + } + + safe_fifo_unref(args->queue); + thread_lock_free(args->lock); + for (i = 0; i < SEARCH_CACHE_SIZE; i++) + { + if (args->cache[i]) + { + free_search_data(args->cache[i]); + free(args->cache[i]); + } + } + free(args); + return NULL; +} |
