#include "common.h" #include "paths.h" #include "ref.h" #include "thread.h" #include "safe_fifo.h" #include "strutil.h" #include #include #include #include #include #include #include #include #include #include #include #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; 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; }