You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

872 lines
22 KiB

/* -*- coding: utf-8 -*-
* ----------------------------------------------------------------------
* Copyright © 2013-2014, RedJack, LLC.
* All rights reserved.
*
* Please see the COPYING file in this distribution for license details.
* ----------------------------------------------------------------------
*/
#include <assert.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#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 <stdio.h>
#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);
cork_delete(struct cork_path, 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, 0) == '/') {
/* 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 it's 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);
cork_delete(struct cork_path_list, 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);
cork_delete(struct cork_file, 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);
cork_delete(struct cork_file_list, 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);
}
}