/* -*- coding: utf-8 -*- * ---------------------------------------------------------------------- * Copyright © 2013, RedJack, LLC. * All rights reserved. * * Please see the COPYING file in this distribution for license details. * ---------------------------------------------------------------------- */ #include #include #include #include #include #include #include #include #include "libcork/core/attributes.h" #include "libcork/core/error.h" #include "libcork/core/types.h" #include "libcork/ds/array.h" #include "libcork/ds/buffer.h" #include "libcork/helpers/errors.h" #include "libcork/helpers/posix.h" #include "libcork/os/files.h" #include "libcork/os/subprocess.h" #if !defined(CORK_DEBUG_FILES) #define CORK_DEBUG_FILES 0 #endif #if CORK_DEBUG_FILES #include #define DEBUG(...) fprintf(stderr, __VA_ARGS__) #else #define DEBUG(...) /* no debug messages */ #endif /*----------------------------------------------------------------------- * Paths */ struct cork_path { struct cork_buffer given; }; static struct cork_path * cork_path_new_internal(const char *str, size_t length) { struct cork_path *path = cork_new(struct cork_path); cork_buffer_init(&path->given); if (length == 0) { cork_buffer_ensure_size(&path->given, 16); cork_buffer_set(&path->given, "", 0); } else { cork_buffer_set(&path->given, str, length); } return path; } struct cork_path * cork_path_new(const char *source) { return cork_path_new_internal(source, source == NULL? 0: strlen(source)); } struct cork_path * cork_path_clone(const struct cork_path *other) { return cork_path_new_internal(other->given.buf, other->given.size); } void cork_path_free(struct cork_path *path) { cork_buffer_done(&path->given); free(path); } void cork_path_set(struct cork_path *path, const char *content) { if (content == NULL) { cork_buffer_clear(&path->given); } else { cork_buffer_set_string(&path->given, content); } } const char * cork_path_get(const struct cork_path *path) { return path->given.buf; } #define cork_path_get(path) ((const char *) (path)->given.buf) #define cork_path_size(path) ((path)->given.size) #define cork_path_truncate(path, size) \ (cork_buffer_truncate(&(path)->given, (size))) int cork_path_set_cwd(struct cork_path *path) { cork_buffer_ensure_size(&path->given, PATH_MAX); rip_check_posix(getcwd(path->given.buf, PATH_MAX)); path->given.size = strlen(path->given.buf); return 0; } struct cork_path * cork_path_cwd(void) { struct cork_path *path = cork_path_new(NULL); ei_check(cork_path_set_cwd(path)); return path; error: cork_path_free(path); return NULL; } int cork_path_set_absolute(struct cork_path *path) { struct cork_buffer buf; if (path->given.size > 0 && cork_buffer_char(&path->given, path->given.size - 1) == '/') { /* The path is already absolute */ return 0; } cork_buffer_init(&buf); cork_buffer_ensure_size(&buf, PATH_MAX); ep_check_posix(getcwd(buf.buf, PATH_MAX)); buf.size = strlen(buf.buf); cork_buffer_append(&buf, "/", 1); cork_buffer_append_copy(&buf, &path->given); cork_buffer_done(&path->given); path->given = buf; return 0; error: cork_buffer_done(&buf); return -1; } struct cork_path * cork_path_absolute(const struct cork_path *other) { struct cork_path *path = cork_path_clone(other); ei_check(cork_path_set_absolute(path)); return path; error: cork_path_free(path); return NULL; } void cork_path_append(struct cork_path *path, const char *more) { if (more == NULL || more[0] == '\0') { return; } if (more[0] == '/') { /* If more starts with a "/", then its absolute, and should replace the * contents of the current path. */ cork_buffer_set_string(&path->given, more); } else { /* Otherwise, more is relative, and should be appended to the current * path. If the current given path doesn't end in a "/", then we need * to add one to keep the path well-formed. */ if (path->given.size > 0 && cork_buffer_char(&path->given, path->given.size - 1) != '/') { cork_buffer_append(&path->given, "/", 1); } cork_buffer_append_string(&path->given, more); } } struct cork_path * cork_path_join(const struct cork_path *other, const char *more) { struct cork_path *path = cork_path_clone(other); cork_path_append(path, more); return path; } void cork_path_append_path(struct cork_path *path, const struct cork_path *more) { cork_path_append(path, more->given.buf); } struct cork_path * cork_path_join_path(const struct cork_path *other, const struct cork_path *more) { struct cork_path *path = cork_path_clone(other); cork_path_append_path(path, more); return path; } void cork_path_set_basename(struct cork_path *path) { char *given = path->given.buf; const char *last_slash = strrchr(given, '/'); if (last_slash != NULL) { size_t offset = last_slash - given; size_t basename_length = path->given.size - offset - 1; memmove(given, last_slash + 1, basename_length); given[basename_length] = '\0'; path->given.size = basename_length; } } struct cork_path * cork_path_basename(const struct cork_path *other) { struct cork_path *path = cork_path_clone(other); cork_path_set_basename(path); return path; } void cork_path_set_dirname(struct cork_path *path) { const char *given = path->given.buf; const char *last_slash = strrchr(given, '/'); if (last_slash == NULL) { cork_buffer_clear(&path->given); } else { size_t offset = last_slash - given; if (offset == 0) { /* A special case for the immediate subdirectories of "/" */ cork_buffer_truncate(&path->given, 1); } else { cork_buffer_truncate(&path->given, offset); } } } struct cork_path * cork_path_dirname(const struct cork_path *other) { struct cork_path *path = cork_path_clone(other); cork_path_set_dirname(path); return path; } /*----------------------------------------------------------------------- * Lists of paths */ struct cork_path_list { cork_array(struct cork_path *) array; struct cork_buffer string; }; struct cork_path_list * cork_path_list_new_empty(void) { struct cork_path_list *list = cork_new(struct cork_path_list); cork_array_init(&list->array); cork_buffer_init(&list->string); return list; } void cork_path_list_free(struct cork_path_list *list) { size_t i; for (i = 0; i < cork_array_size(&list->array); i++) { struct cork_path *path = cork_array_at(&list->array, i); cork_path_free(path); } cork_array_done(&list->array); cork_buffer_done(&list->string); free(list); } const char * cork_path_list_to_string(const struct cork_path_list *list) { return list->string.buf; } void cork_path_list_add(struct cork_path_list *list, struct cork_path *path) { cork_array_append(&list->array, path); if (cork_array_size(&list->array) > 1) { cork_buffer_append(&list->string, ":", 1); } cork_buffer_append_string(&list->string, cork_path_get(path)); } size_t cork_path_list_size(const struct cork_path_list *list) { return cork_array_size(&list->array); } const struct cork_path * cork_path_list_get(const struct cork_path_list *list, size_t index) { return cork_array_at(&list->array, index); } static void cork_path_list_append_string(struct cork_path_list *list, const char *str) { struct cork_path *path; const char *curr = str; const char *next; while ((next = strchr(curr, ':')) != NULL) { size_t size = next - curr; path = cork_path_new_internal(curr, size); cork_path_list_add(list, path); curr = next + 1; } path = cork_path_new(curr); cork_path_list_add(list, path); } struct cork_path_list * cork_path_list_new(const char *str) { struct cork_path_list *list = cork_path_list_new_empty(); cork_path_list_append_string(list, str); return list; } /*----------------------------------------------------------------------- * Files */ struct cork_file { struct cork_path *path; struct stat stat; enum cork_file_type type; bool has_stat; }; static void cork_file_init(struct cork_file *file, struct cork_path *path) { file->path = path; file->has_stat = false; } struct cork_file * cork_file_new(const char *path) { return cork_file_new_from_path(cork_path_new(path)); } struct cork_file * cork_file_new_from_path(struct cork_path *path) { struct cork_file *file = cork_new(struct cork_file); cork_file_init(file, path); return file; } static void cork_file_reset(struct cork_file *file) { file->has_stat = false; } static void cork_file_done(struct cork_file *file) { cork_path_free(file->path); } void cork_file_free(struct cork_file *file) { cork_file_done(file); free(file); } const struct cork_path * cork_file_path(struct cork_file *file) { return file->path; } static int cork_file_stat(struct cork_file *file) { if (file->has_stat) { return 0; } else { int rc; rc = stat(cork_path_get(file->path), &file->stat); if (rc == -1) { if (errno == ENOENT || errno == ENOTDIR) { file->type = CORK_FILE_MISSING; file->has_stat = true; return 0; } else { cork_system_error_set(); return -1; } } if (S_ISREG(file->stat.st_mode)) { file->type = CORK_FILE_REGULAR; } else if (S_ISDIR(file->stat.st_mode)) { file->type = CORK_FILE_DIRECTORY; } else if (S_ISLNK(file->stat.st_mode)) { file->type = CORK_FILE_SYMLINK; } else { file->type = CORK_FILE_UNKNOWN; } file->has_stat = true; return 0; } } int cork_file_exists(struct cork_file *file, bool *exists) { rii_check(cork_file_stat(file)); *exists = (file->type != CORK_FILE_MISSING); return 0; } int cork_file_type(struct cork_file *file, enum cork_file_type *type) { rii_check(cork_file_stat(file)); *type = file->type; return 0; } struct cork_file * cork_path_list_find_file(const struct cork_path_list *list, const char *rel_path) { size_t i; size_t count = cork_path_list_size(list); struct cork_file *file; for (i = 0; i < count; i++) { const struct cork_path *path = cork_path_list_get(list, i); struct cork_path *joined = cork_path_join(path, rel_path); bool exists; file = cork_file_new_from_path(joined); ei_check(cork_file_exists(file, &exists)); if (exists) { return file; } else { cork_file_free(file); } } cork_error_set_printf (ENOENT, "%s not found in %s", rel_path, cork_path_list_to_string(list)); return NULL; error: cork_file_free(file); return NULL; } /*----------------------------------------------------------------------- * Directories */ int cork_file_iterate_directory(struct cork_file *file, cork_file_directory_iterator iterator, void *user_data) { DIR *dir = NULL; struct dirent *entry; size_t dir_path_size; struct cork_path *child_path; struct cork_file child_file; rip_check_posix(dir = opendir(cork_path_get(file->path))); child_path = cork_path_clone(file->path); cork_file_init(&child_file, child_path); dir_path_size = cork_path_size(child_path); errno = 0; while ((entry = readdir(dir)) != NULL) { /* Skip the "." and ".." entries */ if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } cork_path_append(child_path, entry->d_name); ei_check(cork_file_stat(&child_file)); /* If the entry is a subdirectory, recurse into it. */ ei_check(iterator(&child_file, entry->d_name, user_data)); /* Remove this entry name from the path buffer. */ cork_path_truncate(child_path, dir_path_size); cork_file_reset(&child_file); /* We have to reset errno to 0 because of the ambiguous way readdir uses * a return value of NULL. Other functions may return normally yet set * errno to a non-zero value. dlopen on Mac OS X is an ogreish example. * Since an error readdir is indicated by returning NULL and setting * errno to indicate the error, then we need to reset it to zero before * each call. We shall assume, perhaps to our great misery, that * functions within this loop do proper error checking and act * accordingly. */ errno = 0; } /* Check errno immediately after the while loop terminates */ if (CORK_UNLIKELY(errno != 0)) { cork_system_error_set(); goto error; } cork_file_done(&child_file); rii_check_posix(closedir(dir)); return 0; error: cork_file_done(&child_file); rii_check_posix(closedir(dir)); return -1; } static int cork_file_mkdir_one(struct cork_file *file, cork_file_mode mode, unsigned int flags) { DEBUG("mkdir %s\n", cork_path_get(file->path)); /* First check if the directory already exists. */ rii_check(cork_file_stat(file)); if (file->type == CORK_FILE_DIRECTORY) { DEBUG(" Already exists!\n"); if (!(flags & CORK_FILE_PERMISSIVE)) { cork_system_error_set_explicit(EEXIST); return -1; } else { return 0; } } else if (file->type != CORK_FILE_MISSING) { DEBUG(" Exists and not a directory!\n"); cork_system_error_set_explicit(EEXIST); return -1; } /* If the caller asked for a recursive mkdir, then make sure the parent * directory exists. */ if (flags & CORK_FILE_RECURSIVE) { struct cork_path *parent = cork_path_dirname(file->path); DEBUG(" Checking parent %s\n", cork_path_get(parent)); if (parent->given.size == 0) { /* There is no parent; we're either at the filesystem root (for an * absolute path) or the current directory (for a relative one). * Either way, we can assume it already exists. */ cork_path_free(parent); } else { int rc; struct cork_file parent_file; cork_file_init(&parent_file, parent); rc = cork_file_mkdir_one (&parent_file, mode, flags | CORK_FILE_PERMISSIVE); cork_file_done(&parent_file); rii_check(rc); } } /* Create the directory already! */ DEBUG(" Creating %s\n", cork_path_get(file->path)); rii_check_posix(mkdir(cork_path_get(file->path), mode)); return 0; } int cork_file_mkdir(struct cork_file *file, cork_file_mode mode, unsigned int flags) { return cork_file_mkdir_one(file, mode, flags); } static int cork_file_remove_iterator(struct cork_file *file, const char *rel_name, void *user_data) { unsigned int *flags = user_data; return cork_file_remove(file, *flags); } int cork_file_remove(struct cork_file *file, unsigned int flags) { DEBUG("rm %s\n", cork_path_get(file->path)); rii_check(cork_file_stat(file)); if (file->type == CORK_FILE_MISSING) { if (flags & CORK_FILE_PERMISSIVE) { return 0; } else { cork_system_error_set_explicit(ENOENT); return -1; } } else if (file->type == CORK_FILE_DIRECTORY) { if (flags & CORK_FILE_RECURSIVE) { /* The user asked that we delete the contents of the directory * first. */ rii_check(cork_file_iterate_directory (file, cork_file_remove_iterator, &flags)); } rii_check_posix(rmdir(cork_path_get(file->path))); return 0; } else { rii_check(unlink(cork_path_get(file->path))); return 0; } } /*----------------------------------------------------------------------- * Lists of files */ struct cork_file_list { cork_array(struct cork_file *) array; }; struct cork_file_list * cork_file_list_new_empty(void) { struct cork_file_list *list = cork_new(struct cork_file_list); cork_array_init(&list->array); return list; } void cork_file_list_free(struct cork_file_list *list) { size_t i; for (i = 0; i < cork_array_size(&list->array); i++) { struct cork_file *file = cork_array_at(&list->array, i); cork_file_free(file); } cork_array_done(&list->array); free(list); } void cork_file_list_add(struct cork_file_list *list, struct cork_file *file) { cork_array_append(&list->array, file); } size_t cork_file_list_size(struct cork_file_list *list) { return cork_array_size(&list->array); } struct cork_file * cork_file_list_get(struct cork_file_list *list, size_t index) { return cork_array_at(&list->array, index); } struct cork_file_list * cork_file_list_new(struct cork_path_list *path_list) { struct cork_file_list *list = cork_file_list_new_empty(); size_t count = cork_path_list_size(path_list); size_t i; for (i = 0; i < count; i++) { const struct cork_path *path = cork_path_list_get(path_list, i); struct cork_file *file = cork_file_new(cork_path_get(path)); cork_array_append(&list->array, file); } return list; } struct cork_file_list * cork_path_list_find_files(const struct cork_path_list *path_list, const char *rel_path) { size_t i; size_t count = cork_path_list_size(path_list); struct cork_file_list *list = cork_file_list_new_empty(); struct cork_file *file; for (i = 0; i < count; i++) { const struct cork_path *path = cork_path_list_get(path_list, i); struct cork_path *joined = cork_path_join(path, rel_path); bool exists; file = cork_file_new_from_path(joined); ei_check(cork_file_exists(file, &exists)); if (exists) { cork_file_list_add(list, file); } else { cork_file_free(file); } } return list; error: cork_file_list_free(list); cork_file_free(file); return NULL; } /*----------------------------------------------------------------------- * Standard paths and path lists */ #define empty_string(str) ((str) == NULL || (str)[0] == '\0') struct cork_path * cork_path_home(void) { const char *path = cork_env_get(NULL, "HOME"); if (empty_string(path)) { cork_undefined("Cannot determine home directory"); return NULL; } else { return cork_path_new(path); } } struct cork_path_list * cork_path_config_paths(void) { struct cork_path_list *list = cork_path_list_new_empty(); const char *var; struct cork_path *path; /* The first entry should be the user's configuration directory. This is * specified by $XDG_CONFIG_HOME, with $HOME/.config as the default. */ var = cork_env_get(NULL, "XDG_CONFIG_HOME"); if (empty_string(var)) { ep_check(path = cork_path_home()); cork_path_append(path, ".config"); cork_path_list_add(list, path); } else { path = cork_path_new(var); cork_path_list_add(list, path); } /* The remaining entries should be the system-wide configuration * directories. These are specified by $XDG_CONFIG_DIRS, with /etc/xdg as * the default. */ var = cork_env_get(NULL, "XDG_CONFIG_DIRS"); if (empty_string(var)) { path = cork_path_new("/etc/xdg"); cork_path_list_add(list, path); } else { cork_path_list_append_string(list, var); } return list; error: cork_path_list_free(list); return NULL; } struct cork_path_list * cork_path_data_paths(void) { struct cork_path_list *list = cork_path_list_new_empty(); const char *var; struct cork_path *path; /* The first entry should be the user's data directory. This is specified * by $XDG_DATA_HOME, with $HOME/.local/share as the default. */ var = cork_env_get(NULL, "XDG_DATA_HOME"); if (empty_string(var)) { ep_check(path = cork_path_home()); cork_path_append(path, ".local/share"); cork_path_list_add(list, path); } else { path = cork_path_new(var); cork_path_list_add(list, path); } /* The remaining entries should be the system-wide configuration * directories. These are specified by $XDG_DATA_DIRS, with * /usr/local/share:/usr/share as the the default. */ var = cork_env_get(NULL, "XDG_DATA_DIRS"); if (empty_string(var)) { path = cork_path_new("/usr/local/share"); cork_path_list_add(list, path); path = cork_path_new("/usr/share"); cork_path_list_add(list, path); } else { cork_path_list_append_string(list, var); } return list; error: cork_path_list_free(list); return NULL; } struct cork_path * cork_path_user_cache_path(void) { const char *var; struct cork_path *path; /* The user's cache directory is specified by $XDG_CACHE_HOME, with * $HOME/.cache as the default. */ var = cork_env_get(NULL, "XDG_CACHE_HOME"); if (empty_string(var)) { rpp_check(path = cork_path_home()); cork_path_append(path, ".cache"); return path; } else { return cork_path_new(var); } } struct cork_path * cork_path_user_runtime_path(void) { const char *var; /* The user's cache directory is specified by $XDG_RUNTIME_DIR, with * no default given by the spec. */ var = cork_env_get(NULL, "XDG_RUNTIME_DIR"); if (empty_string(var)) { cork_undefined("Cannot determine user-specific runtime directory"); return NULL; } else { return cork_path_new(var); } }