diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Cli/Cli.c | 102 | ||||
| -rw-r--r-- | src/Cli/Cli.h | 30 | ||||
| -rw-r--r-- | src/DirectoryWalker/DirectoryWalker.c | 249 | ||||
| -rw-r--r-- | src/DirectoryWalker/DirectoryWalker.h | 8 | ||||
| -rw-r--r-- | src/Dom/Dom.c | 303 | ||||
| -rw-r--r-- | src/Dom/Dom.h | 10 | ||||
| -rw-r--r-- | src/FileProcessor/FileProcessor.c | 396 | ||||
| -rw-r--r-- | src/FileProcessor/FileProcessor.h | 8 | ||||
| -rw-r--r-- | src/Logger/Logger.c | 102 | ||||
| -rw-r--r-- | src/Logger/Logger.h | 31 | ||||
| -rw-r--r-- | src/Logger/LuaLogger.c | 60 | ||||
| -rw-r--r-- | src/Logger/LuaLogger.h | 8 | ||||
| -rw-r--r-- | src/LuaEngine/LuaEngine.c | 273 | ||||
| -rw-r--r-- | src/LuaEngine/LuaEngine.h | 8 | ||||
| -rw-r--r-- | src/Main.c | 52 | ||||
| -rw-r--r-- | src/Path/Path.c | 185 | ||||
| -rw-r--r-- | src/Path/Path.h | 10 | ||||
| -rw-r--r-- | src/Version/Version.c | 3 | ||||
| -rw-r--r-- | src/Version/Version.h | 6 |
19 files changed, 1844 insertions, 0 deletions
diff --git a/src/Cli/Cli.c b/src/Cli/Cli.c new file mode 100644 index 0000000..3648493 --- /dev/null +++ b/src/Cli/Cli.c @@ -0,0 +1,102 @@ +#include "Cli/Cli.h" + +#include "Logger/Logger.h" +#include "Version/Version.h" + +#include <getopt.h> +#include <stdio.h> + +void cli_print_usage(const char *program_name) { + fprintf( + stderr, + "Usage:\n" + " %s FILE [-o OUTPUT]\n" + " %s -d DIRECTORY [-o OUTPUT] [-f]\n\n" + "Options:\n" + " -d, --directory DIRECTORY Process a directory recursively\n" + " -o, --output OUTPUT Set the output path\n" + " -f, --overwrite Overwrite an existing output directory\n" + " -l, --log-level LEVEL error, warning, info, debug, or silent\n" + " -v, --version Show the program version\n" + " -h, --help Show this help\n", + program_name, program_name); +} + +static void cli_print_version(void) { printf("yapssg %s\n", yapssg_version); } + +static const char *strip_optional_equals(const char *value) { + return value != NULL && value[0] == '=' ? value + 1 : value; +} + +CliParseResult cli_parse(int argc, char **argv, CliOptions *options) { + static const struct option long_options[] = { + {"directory", required_argument, NULL, 'd'}, + {"output", required_argument, NULL, 'o'}, + {"overwrite", no_argument, NULL, 'f'}, + {"force", no_argument, NULL, 'f'}, + {"log-level", required_argument, NULL, 'l'}, + {"version", no_argument, NULL, 'v'}, + {"help", no_argument, NULL, 'h'}, + {NULL, 0, NULL, 0}}; + const char *directory = NULL; + int option; + + options->input_type = INPUT_TYPE_NONE; + options->input_path = NULL; + options->output_path = NULL; + options->overwrite_output = 0; + options->log_level = LOG_LEVEL_INFO; + + while ((option = getopt_long(argc, argv, "d:o:fl:vh", long_options, NULL)) != + -1) { + switch (option) { + case 'd': + directory = strip_optional_equals(optarg); + break; + case 'o': + options->output_path = strip_optional_equals(optarg); + break; + case 'f': + options->overwrite_output = 1; + break; + case 'l': + if (logger_parse_level(optarg, &options->log_level) != 0) { + logger_error("unknown log level '%s'", optarg); + return CLI_PARSE_ERROR; + } + break; + case 'v': + cli_print_version(); + return CLI_PARSE_VERSION; + case 'h': + cli_print_usage(argv[0]); + return CLI_PARSE_HELP; + default: + cli_print_usage(argv[0]); + return CLI_PARSE_ERROR; + } + } + + if (directory != NULL) { + if (optind != argc || directory[0] == '\0') { + logger_error("a directory cannot be combined with a file"); + return CLI_PARSE_ERROR; + } + options->input_type = INPUT_TYPE_DIRECTORY; + options->input_path = directory; + } else { + if (optind + 1 != argc) { + cli_print_usage(argv[0]); + return CLI_PARSE_ERROR; + } + options->input_type = INPUT_TYPE_FILE; + options->input_path = argv[optind]; + } + + if (options->output_path != NULL && options->output_path[0] == '\0') { + logger_error("output path cannot be empty"); + return CLI_PARSE_ERROR; + } + + return CLI_PARSE_OK; +} diff --git a/src/Cli/Cli.h b/src/Cli/Cli.h new file mode 100644 index 0000000..9695fd4 --- /dev/null +++ b/src/Cli/Cli.h @@ -0,0 +1,30 @@ +#ifndef YAPSSG_CLI_H +#define YAPSSG_CLI_H + +#include "Logger/Logger.h" + +typedef enum { + INPUT_TYPE_NONE, + INPUT_TYPE_FILE, + INPUT_TYPE_DIRECTORY +} InputType; + +typedef enum { + CLI_PARSE_ERROR = -1, + CLI_PARSE_OK, + CLI_PARSE_HELP, + CLI_PARSE_VERSION +} CliParseResult; + +typedef struct { + InputType input_type; + const char *input_path; + const char *output_path; + int overwrite_output; + LogLevel log_level; +} CliOptions; + +CliParseResult cli_parse(int argc, char **argv, CliOptions *options); +void cli_print_usage(const char *program_name); + +#endif diff --git a/src/DirectoryWalker/DirectoryWalker.c b/src/DirectoryWalker/DirectoryWalker.c new file mode 100644 index 0000000..dc3729a --- /dev/null +++ b/src/DirectoryWalker/DirectoryWalker.c @@ -0,0 +1,249 @@ +#define _POSIX_C_SOURCE 200809L + +#include "DirectoryWalker/DirectoryWalker.h" + +#include "Path/Path.h" + +#include <dirent.h> +#include <errno.h> +#include <lauxlib.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> + +#define WALKER_METATABLE "yapssg.directory_walker" + +typedef struct { + DIR *directory; + char *absolute_path; + char *display_path; + size_t depth; +} WalkFrame; + +typedef struct { + WalkFrame *frames; + size_t length; + size_t capacity; +} DirectoryWalker; + +static void close_frame(WalkFrame *frame) { + if (frame->directory != NULL) { + closedir(frame->directory); + } + free(frame->absolute_path); + free(frame->display_path); +} + +static void clear_walker(DirectoryWalker *walker) { + while (walker->length > 0) { + close_frame(&walker->frames[--walker->length]); + } + free(walker->frames); + walker->frames = NULL; + walker->capacity = 0; +} + +static int walker_gc(lua_State *lua) { + DirectoryWalker *walker = lua_touserdata(lua, 1); + + clear_walker(walker); + return 0; +} + +static int grow_walker(DirectoryWalker *walker) { + size_t larger_capacity; + WalkFrame *larger_frames; + + if (walker->capacity > SIZE_MAX / (2 * sizeof(*walker->frames))) { + errno = ENOMEM; + return -1; + } + + larger_capacity = walker->capacity == 0 ? 8 : walker->capacity * 2; + larger_frames = + realloc(walker->frames, larger_capacity * sizeof(*walker->frames)); + if (larger_frames == NULL) { + errno = ENOMEM; + return -1; + } + + walker->frames = larger_frames; + walker->capacity = larger_capacity; + return 0; +} + +static int push_frame(DirectoryWalker *walker, char *absolute_path, + char *display_path, size_t depth) { + DIR *directory = opendir(absolute_path); + WalkFrame *frame; + + if (directory == NULL) { + return -1; + } + if (walker->length == walker->capacity && grow_walker(walker) != 0) { + int error_number = errno; + + closedir(directory); + errno = error_number; + return -1; + } + + frame = &walker->frames[walker->length++]; + frame->directory = directory; + frame->absolute_path = absolute_path; + frame->display_path = display_path; + frame->depth = depth; + return 0; +} + +static const char *entry_type(mode_t mode) { + if (S_ISREG(mode)) { + return "file"; + } + if (S_ISDIR(mode)) { + return "directory"; + } + if (S_ISLNK(mode)) { + return "symlink"; + } + return "other"; +} + +static void push_entry(lua_State *lua, const char *path, const char *name, + const struct stat *status, size_t depth) { + lua_newtable(lua); + + lua_pushstring(lua, path); + lua_setfield(lua, -2, "path"); + + lua_pushstring(lua, name); + lua_setfield(lua, -2, "name"); + + lua_pushstring(lua, entry_type(status->st_mode)); + lua_setfield(lua, -2, "type"); + + lua_pushnumber(lua, (lua_Number)depth); + lua_setfield(lua, -2, "depth"); + + if (S_ISREG(status->st_mode)) { + lua_pushnumber(lua, (lua_Number)status->st_size); + lua_setfield(lua, -2, "size"); + } +} + +static int walker_next(lua_State *lua) { + DirectoryWalker *walker = lua_touserdata(lua, lua_upvalueindex(1)); + + while (walker->length > 0) { + WalkFrame *frame = &walker->frames[walker->length - 1]; + struct dirent *entry; + char *absolute_path; + char *display_path; + struct stat status; + size_t depth; + + errno = 0; + entry = readdir(frame->directory); + if (entry == NULL) { + int error_number = errno; + + close_frame(frame); + --walker->length; + if (error_number != 0) { + clear_walker(walker); + return luaL_error(lua, "cannot read directory while walking: %s", + strerror(error_number)); + } + continue; + } + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { + continue; + } + + absolute_path = path_join(frame->absolute_path, entry->d_name); + display_path = path_join(frame->display_path, entry->d_name); + if (absolute_path == NULL || display_path == NULL) { + free(absolute_path); + free(display_path); + clear_walker(walker); + return luaL_error(lua, "unable to allocate directory-walk path"); + } + if (lstat(absolute_path, &status) != 0) { + int error_number = errno; + + free(absolute_path); + free(display_path); + clear_walker(walker); + return luaL_error(lua, "cannot inspect directory entry: %s", + strerror(error_number)); + } + + depth = frame->depth + 1; + push_entry(lua, display_path, entry->d_name, &status, depth); + + if (S_ISDIR(status.st_mode)) { + if (push_frame(walker, absolute_path, display_path, depth) != 0) { + int error_number = errno; + + free(absolute_path); + free(display_path); + clear_walker(walker); + return luaL_error(lua, "cannot open directory while walking: %s", + strerror(error_number)); + } + } else { + free(absolute_path); + free(display_path); + } + return 1; + } + clear_walker(walker); + return 0; +} + +static int walk(lua_State *lua) { + const char *input_path = lua_tostring(lua, lua_upvalueindex(1)); + const char *requested_path = luaL_checkstring(lua, 1); + char *absolute_path = path_resolve_from_file(input_path, requested_path); + char *display_path = path_join("", requested_path); + DirectoryWalker *walker; + + if (absolute_path == NULL || display_path == NULL) { + free(absolute_path); + free(display_path); + return luaL_error(lua, "unable to allocate directory-walk root"); + } + + walker = lua_newuserdata(lua, sizeof(*walker)); + memset(walker, 0, sizeof(*walker)); + luaL_getmetatable(lua, WALKER_METATABLE); + lua_setmetatable(lua, -2); + + if (push_frame(walker, absolute_path, display_path, 0) != 0) { + int error_number = errno; + + free(absolute_path); + free(display_path); + return luaL_error(lua, "cannot open directory '%s': %s", requested_path, + strerror(error_number)); + } + + lua_pushcclosure(lua, walker_next, 1); + return 1; +} + +void directory_walker_register(lua_State *lua, const char *input_path) { + if (luaL_newmetatable(lua, WALKER_METATABLE)) { + lua_pushcfunction(lua, walker_gc); + lua_setfield(lua, -2, "__gc"); + } + lua_pop(lua, 1); + + lua_newtable(lua); + lua_pushstring(lua, input_path); + lua_pushcclosure(lua, walk, 1); + lua_setfield(lua, -2, "walk"); + lua_setglobal(lua, "fs"); +} diff --git a/src/DirectoryWalker/DirectoryWalker.h b/src/DirectoryWalker/DirectoryWalker.h new file mode 100644 index 0000000..94dcbf6 --- /dev/null +++ b/src/DirectoryWalker/DirectoryWalker.h @@ -0,0 +1,8 @@ +#ifndef YAPSSG_DIRECTORY_WALKER_H +#define YAPSSG_DIRECTORY_WALKER_H + +#include <lua.h> + +void directory_walker_register(lua_State *lua, const char *input_path); + +#endif diff --git a/src/Dom/Dom.c b/src/Dom/Dom.c new file mode 100644 index 0000000..ede7ddd --- /dev/null +++ b/src/Dom/Dom.c @@ -0,0 +1,303 @@ +#include "Dom/Dom.h" + +#include <lauxlib.h> +#include <limits.h> +#include <stddef.h> +#include <string.h> +#include <strings.h> + +static int absolute_index(lua_State *lua, int index) { + return index < 0 ? lua_gettop(lua) + index + 1 : index; +} + +static int is_lua_element(const xmlNode *node) { + return node->type == XML_ELEMENT_NODE && + strcasecmp((const char *)node->name, "lua") == 0; +} + +static void set_string_field(lua_State *lua, int table_index, const char *name, + const char *value) { + lua_pushstring(lua, value); + lua_setfield(lua, table_index, name); +} + +static int has_structured_children(const xmlNode *node) { + const xmlNode *child; + + for (child = node->children; child != NULL; child = child->next) { + if (is_lua_element(child)) { + continue; + } + if (child->type != XML_TEXT_NODE && child->type != XML_CDATA_SECTION_NODE) { + return 1; + } + } + return 0; +} + +static void push_element(lua_State *lua, const xmlNode *node); + +static void push_special_node(lua_State *lua, const xmlNode *node) { + lua_newtable(lua); + if (node->type == XML_COMMENT_NODE) { + set_string_field(lua, -2, "kind", "comment"); + } else { + set_string_field(lua, -2, "kind", "processing_instruction"); + set_string_field(lua, -2, "target", (const char *)node->name); + } + set_string_field(lua, -2, "text", + node->content == NULL ? "" : (const char *)node->content); +} + +static void add_child_alias(lua_State *lua, int element_index, + const xmlNode *child, int child_index) { + const char *name = (const char *)child->name; + + lua_getfield(lua, element_index, name); + if (lua_isnil(lua, -1)) { + lua_pop(lua, 1); + lua_pushvalue(lua, child_index); + lua_setfield(lua, element_index, name); + return; + } + lua_pop(lua, 1); +} + +static void push_children(lua_State *lua, const xmlNode *node, + int element_index, int include_text) { + const xmlNode *child; + int children_index; + int array_index = 1; + + lua_newtable(lua); + children_index = lua_gettop(lua); + + for (child = node->children; child != NULL; child = child->next) { + if (is_lua_element(child)) { + continue; + } + + if (child->type == XML_ELEMENT_NODE) { + int child_index; + + push_element(lua, child); + child_index = lua_gettop(lua); + lua_pushvalue(lua, child_index); + lua_rawseti(lua, children_index, array_index++); + add_child_alias(lua, element_index, child, child_index); + lua_pop(lua, 1); + } else if (include_text && (child->type == XML_TEXT_NODE || + child->type == XML_CDATA_SECTION_NODE)) { + lua_pushstring( + lua, child->content == NULL ? "" : (const char *)child->content); + lua_rawseti(lua, children_index, array_index++); + } else if (child->type == XML_COMMENT_NODE || child->type == XML_PI_NODE) { + push_special_node(lua, child); + lua_rawseti(lua, children_index, array_index++); + } + } + + lua_setfield(lua, element_index, "children"); +} + +static void push_attributes(lua_State *lua, const xmlNode *node, + int element_index) { + const xmlAttr *attribute; + + lua_newtable(lua); + for (attribute = node->properties; attribute != NULL; + attribute = attribute->next) { + xmlChar *value = xmlNodeListGetString(node->doc, attribute->children, 1); + + lua_pushstring(lua, value == NULL ? "" : (const char *)value); + lua_setfield(lua, -2, (const char *)attribute->name); + xmlFree(value); + } + lua_setfield(lua, element_index, "attributes"); +} + +static void push_text(lua_State *lua, const xmlNode *node, int element_index) { + const xmlNode *child; + luaL_Buffer buffer; + + luaL_buffinit(lua, &buffer); + for (child = node->children; child != NULL; child = child->next) { + if (child->type == XML_TEXT_NODE || child->type == XML_CDATA_SECTION_NODE) { + luaL_addstring( + &buffer, child->content == NULL ? "" : (const char *)child->content); + } + } + luaL_pushresult(&buffer); + lua_setfield(lua, element_index, "text"); +} + +static void push_element(lua_State *lua, const xmlNode *node) { + int element_index; + int structured_children = has_structured_children(node); + + lua_newtable(lua); + element_index = lua_gettop(lua); + set_string_field(lua, element_index, "tag", (const char *)node->name); + push_attributes(lua, node, element_index); + push_children(lua, node, element_index, structured_children); + if (!structured_children) { + push_text(lua, node, element_index); + } +} + +void dom_push_page(lua_State *lua, const xmlNode *root) { + push_element(lua, root); +} + +static xmlNodePtr node_from_table(lua_State *lua, int table_index, int *valid); + +static int add_attributes(lua_State *lua, int table_index, xmlNodePtr node) { + int attributes_index; + int valid = 1; + + lua_getfield(lua, table_index, "attributes"); + attributes_index = lua_gettop(lua); + if (lua_istable(lua, -1)) { + lua_pushnil(lua); + while (valid && lua_next(lua, -2) != 0) { + if (lua_type(lua, -2) != LUA_TSTRING || + lua_type(lua, -1) != LUA_TSTRING || + xmlNewProp(node, BAD_CAST lua_tostring(lua, -2), + BAD_CAST lua_tostring(lua, -1)) == NULL) { + valid = 0; + } + lua_pop(lua, 1); + } + } + lua_settop(lua, attributes_index - 1); + return valid ? 0 : -1; +} + +static int append_lua_child(lua_State *lua, xmlNodePtr parent, + int child_index) { + xmlNodePtr child = NULL; + int valid = 1; + + if (lua_type(lua, child_index) == LUA_TSTRING) { + child = xmlNewText(BAD_CAST lua_tostring(lua, child_index)); + } else if (lua_istable(lua, child_index)) { + child = node_from_table(lua, child_index, &valid); + } else { + valid = 0; + } + + if (!valid || child == NULL) { + xmlFreeNode(child); + return -1; + } + if (xmlAddChild(parent, child) == NULL) { + xmlFreeNode(child); + return -1; + } + return 0; +} + +static int add_children(lua_State *lua, int table_index, xmlNodePtr node) { + size_t index; + size_t length; + int result = 0; + + lua_getfield(lua, table_index, "children"); + length = lua_istable(lua, -1) ? lua_objlen(lua, -1) : 0; + + if (length > INT_MAX) { + lua_pop(lua, 1); + return -1; + } + + for (index = 1; result == 0 && index <= length; ++index) { + lua_rawgeti(lua, -1, (int)index); + result = append_lua_child(lua, node, lua_gettop(lua)); + lua_pop(lua, 1); + } + lua_pop(lua, 1); + + if (result == 0 && length == 0) { + lua_getfield(lua, table_index, "text"); + if (lua_type(lua, -1) == LUA_TSTRING) { + xmlNodePtr text = xmlNewText(BAD_CAST lua_tostring(lua, -1)); + + if (text == NULL || xmlAddChild(node, text) == NULL) { + xmlFreeNode(text); + result = -1; + } + } + lua_pop(lua, 1); + } + return result; +} + +static xmlNodePtr special_node_from_table(lua_State *lua, int table_index, + const char *kind, int *valid) { + xmlNodePtr node; + const char *text; + + lua_getfield(lua, table_index, "text"); + text = lua_tostring(lua, -1); + + if (strcmp(kind, "comment") == 0) { + node = xmlNewComment(BAD_CAST(text == NULL ? "" : text)); + } else if (strcmp(kind, "processing_instruction") == 0) { + const char *target; + + lua_getfield(lua, table_index, "target"); + target = lua_tostring(lua, -1); + node = target == NULL || target[0] == '\0' + ? NULL + : xmlNewPI(BAD_CAST target, BAD_CAST(text == NULL ? "" : text)); + lua_pop(lua, 1); + } else { + node = NULL; + } + + lua_pop(lua, 1); + if (node == NULL) { + *valid = 0; + } + return node; +} + +static xmlNodePtr node_from_table(lua_State *lua, int table_index, int *valid) { + const char *kind; + const char *tag; + xmlNodePtr node; + + table_index = absolute_index(lua, table_index); + lua_getfield(lua, table_index, "kind"); + kind = lua_tostring(lua, -1); + if (kind != NULL) { + node = special_node_from_table(lua, table_index, kind, valid); + lua_pop(lua, 1); + return node; + } + lua_pop(lua, 1); + + lua_getfield(lua, table_index, "tag"); + tag = lua_tostring(lua, -1); + node = tag == NULL || tag[0] == '\0' ? NULL : xmlNewNode(NULL, BAD_CAST tag); + lua_pop(lua, 1); + + if (node == NULL || add_attributes(lua, table_index, node) != 0 || + add_children(lua, table_index, node) != 0) { + xmlFreeNode(node); + *valid = 0; + return NULL; + } + return node; +} + +xmlNodePtr dom_page_from_lua(lua_State *lua, int table_index) { + int valid = 1; + xmlNodePtr root = node_from_table(lua, table_index, &valid); + + if (!valid) { + xmlFreeNode(root); + return NULL; + } + return root; +} diff --git a/src/Dom/Dom.h b/src/Dom/Dom.h new file mode 100644 index 0000000..06bd1ff --- /dev/null +++ b/src/Dom/Dom.h @@ -0,0 +1,10 @@ +#ifndef YAPSSG_DOM_H +#define YAPSSG_DOM_H + +#include <libxml/tree.h> +#include <lua.h> + +void dom_push_page(lua_State *lua, const xmlNode *root); +xmlNodePtr dom_page_from_lua(lua_State *lua, int table_index); + +#endif diff --git a/src/FileProcessor/FileProcessor.c b/src/FileProcessor/FileProcessor.c new file mode 100644 index 0000000..d856fb4 --- /dev/null +++ b/src/FileProcessor/FileProcessor.c @@ -0,0 +1,396 @@ +#define _POSIX_C_SOURCE 200809L +#define _XOPEN_SOURCE 700 + +#include "FileProcessor/FileProcessor.h" + +#include "Logger/Logger.h" +#include "LuaEngine/LuaEngine.h" +#include "Path/Path.h" + +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <libxml/HTMLparser.h> +#include <libxml/HTMLtree.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#define COPY_BUFFER_SIZE 16384 + +static int has_html_extension(const char *path) { + const char *extension = strrchr(path, '.'); + + return extension != NULL && (strcasecmp(extension, ".html") == 0 || + strcasecmp(extension, ".htm") == 0); +} + +static void report_copy_error(const char *input_path, const char *output_path, + int error_number) { + logger_error("cannot copy %s to %s: %s", input_path, output_path, + strerror(error_number)); +} + +static int write_all(int file, const char *buffer, size_t length) { + size_t offset = 0; + + while (offset < length) { + ssize_t written = write(file, buffer + offset, length - offset); + + if (written > 0) { + offset += (size_t)written; + } else if (written == 0) { + errno = EIO; + return -1; + } else if (errno != EINTR) { + return -1; + } + } + return 0; +} + +static int copy_file_data(int input, int output) { + char buffer[COPY_BUFFER_SIZE]; + + for (;;) { + ssize_t count = read(input, buffer, sizeof(buffer)); + + if (count > 0 && write_all(output, buffer, (size_t)count) != 0) { + return -1; + } + if (count == 0) { + return 0; + } + if (count < 0 && errno != EINTR) { + return -1; + } + } +} + +static int copy_regular_file(const char *input_path, const char *output_path, + mode_t mode) { + int input = open(input_path, O_RDONLY); + int output; + int result; + int error_number; + + logger_debug("copying file %s -> %s", input_path, output_path); + + if (input < 0) { + report_copy_error(input_path, output_path, errno); + return -1; + } + + output = open(output_path, O_WRONLY | O_CREAT | O_TRUNC, mode & 0777); + if (output < 0) { + error_number = errno; + close(input); + report_copy_error(input_path, output_path, error_number); + return -1; + } + + result = copy_file_data(input, output); + error_number = result == 0 ? 0 : errno; + + if (close(output) != 0 && result == 0) { + result = -1; + error_number = errno; + } + if (close(input) != 0 && result == 0) { + result = -1; + error_number = errno; + } + + if (result != 0) { + report_copy_error(input_path, output_path, error_number); + } + return result; +} + +int process_file(const char *input_path, const char *output_path) { + const int parse_options = HTML_PARSE_RECOVER | HTML_PARSE_NONET | + HTML_PARSE_NOERROR | HTML_PARSE_NOWARNING; + struct stat status; + htmlDocPtr document; + int saved; + + if (stat(input_path, &status) != 0) { + logger_error("cannot inspect %s: %s", input_path, strerror(errno)); + return -1; + } + if (!S_ISREG(status.st_mode)) { + logger_error("input is not a regular file: %s", input_path); + return -1; + } + + logger_info("processing HTML %s -> %s", input_path, output_path); + document = htmlReadFile(input_path, NULL, parse_options); + if (document == NULL) { + logger_error("unable to parse HTML file: %s", input_path); + return -1; + } + + if (process_html_document(document, input_path) != 0) { + xmlFreeDoc(document); + return -1; + } + + saved = htmlSaveFileFormat(output_path, document, "UTF-8", 1); + xmlFreeDoc(document); + if (saved < 0) { + logger_error("unable to write HTML file: %s", output_path); + return -1; + } + return 0; +} + +static int copy_symlink(const char *input_path, const char *output_path, + const struct stat *status) { + size_t capacity = status->st_size > 0 ? (size_t)status->st_size + 1 : 256; + char *target = malloc(capacity + 1); + ssize_t length = -1; + + logger_debug("copying symbolic link %s -> %s", input_path, output_path); + + if (target == NULL) { + logger_error("unable to allocate symbolic-link buffer"); + return -1; + } + + while (length < 0) { + ssize_t read_length = readlink(input_path, target, capacity); + + if (read_length < 0) { + logger_error("cannot read symbolic link %s: %s", input_path, + strerror(errno)); + free(target); + return -1; + } + if ((size_t)read_length < capacity) { + length = read_length; + } else { + size_t larger_capacity; + char *larger_target; + + if (capacity > (SIZE_MAX - 1) / 2) { + logger_error("unable to allocate symbolic-link buffer"); + free(target); + return -1; + } + larger_capacity = capacity * 2; + larger_target = realloc(target, larger_capacity + 1); + if (larger_target == NULL) { + logger_error("unable to allocate symbolic-link buffer"); + free(target); + return -1; + } + target = larger_target; + capacity = larger_capacity; + } + } + + target[length] = '\0'; + if (symlink(target, output_path) != 0) { + logger_error("cannot copy symbolic link %s: %s", input_path, + strerror(errno)); + free(target); + return -1; + } + + free(target); + return 0; +} + +static int copy_tree(const char *input_path, const char *output_path) { + struct stat status; + DIR *directory; + int result = 0; + + if (lstat(input_path, &status) != 0) { + logger_error("cannot inspect %s: %s", input_path, strerror(errno)); + return -1; + } + if (S_ISLNK(status.st_mode)) { + return copy_symlink(input_path, output_path, &status); + } + if (S_ISREG(status.st_mode)) { + return has_html_extension(input_path) + ? process_file(input_path, output_path) + : copy_regular_file(input_path, output_path, status.st_mode); + } + if (!S_ISDIR(status.st_mode)) { + logger_error("unsupported file type: %s", input_path); + return -1; + } + + if (mkdir(output_path, status.st_mode & 0777) != 0) { + logger_error("cannot create directory %s: %s", output_path, + strerror(errno)); + return -1; + } + + directory = opendir(input_path); + if (directory == NULL) { + logger_error("cannot open directory %s: %s", input_path, strerror(errno)); + return -1; + } + + while (result == 0) { + struct dirent *entry; + char *input_child; + char *output_child; + + errno = 0; + entry = readdir(directory); + if (entry == NULL) { + if (errno != 0) { + logger_error("cannot read directory %s: %s", input_path, + strerror(errno)); + result = -1; + } + break; + } + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { + continue; + } + + input_child = path_join(input_path, entry->d_name); + output_child = path_join(output_path, entry->d_name); + if (input_child == NULL || output_child == NULL) { + logger_error("unable to allocate child path"); + result = -1; + } else { + result = copy_tree(input_child, output_child); + } + free(input_child); + free(output_child); + } + + if (closedir(directory) != 0 && result == 0) { + logger_error("cannot close directory %s: %s", input_path, strerror(errno)); + result = -1; + } + return result; +} + +static int remove_tree(const char *path) { + struct stat status; + DIR *directory; + int result = 0; + + if (lstat(path, &status) != 0) { + if (errno == ENOENT) { + return 0; + } + logger_error("cannot inspect output %s: %s", path, strerror(errno)); + return -1; + } + + if (!S_ISDIR(status.st_mode) || S_ISLNK(status.st_mode)) { + if (unlink(path) != 0) { + logger_error("cannot remove output %s: %s", path, strerror(errno)); + return -1; + } + return 0; + } + + directory = opendir(path); + if (directory == NULL) { + logger_error("cannot open output directory %s: %s", path, strerror(errno)); + return -1; + } + + while (result == 0) { + struct dirent *entry; + char *child; + + errno = 0; + entry = readdir(directory); + if (entry == NULL) { + if (errno != 0) { + logger_error("cannot read output directory %s: %s", path, + strerror(errno)); + result = -1; + } + break; + } + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { + continue; + } + + child = path_join(path, entry->d_name); + if (child == NULL) { + logger_error("unable to allocate output child path"); + result = -1; + } else { + result = remove_tree(child); + } + free(child); + } + + if (closedir(directory) != 0 && result == 0) { + logger_error("cannot close output directory %s: %s", path, strerror(errno)); + result = -1; + } + if (result == 0 && rmdir(path) != 0) { + logger_error("cannot remove output directory %s: %s", path, + strerror(errno)); + result = -1; + } + return result; +} + +int process_directory(const char *input_path, const char *output_path, + int overwrite_output) { + struct stat status; + struct stat output_status; + char *resolved_input; + char *resolved_output; + int result; + + if (stat(input_path, &status) != 0) { + logger_error("cannot inspect %s: %s", input_path, strerror(errno)); + return -1; + } + if (!S_ISDIR(status.st_mode)) { + logger_error("input is not a directory: %s", input_path); + return -1; + } + + resolved_input = realpath(input_path, NULL); + if (lstat(output_path, &output_status) == 0 && + !S_ISLNK(output_status.st_mode)) { + resolved_output = realpath(output_path, NULL); + } else { + resolved_output = path_resolve_new(output_path); + } + if (resolved_input == NULL || resolved_output == NULL) { + logger_error("cannot resolve input or output path"); + free(resolved_input); + free(resolved_output); + return -1; + } + + if (path_is_within(resolved_input, resolved_output)) { + logger_error("output directory cannot be inside input: %s", output_path); + result = -1; + } else if (overwrite_output && + path_is_within(resolved_output, resolved_input)) { + logger_error("output directory cannot contain input: %s", output_path); + result = -1; + } else if (overwrite_output && remove_tree(output_path) != 0) { + result = -1; + } else { + logger_info("processing directory %s -> %s", input_path, output_path); + result = copy_tree(input_path, output_path); + } + + free(resolved_input); + free(resolved_output); + return result; +} diff --git a/src/FileProcessor/FileProcessor.h b/src/FileProcessor/FileProcessor.h new file mode 100644 index 0000000..90027d7 --- /dev/null +++ b/src/FileProcessor/FileProcessor.h @@ -0,0 +1,8 @@ +#ifndef YAPSSG_FILE_PROCESSOR_H +#define YAPSSG_FILE_PROCESSOR_H + +int process_file(const char *input_path, const char *output_path); +int process_directory(const char *input_path, const char *output_path, + int overwrite_output); + +#endif diff --git a/src/Logger/Logger.c b/src/Logger/Logger.c new file mode 100644 index 0000000..c43b24b --- /dev/null +++ b/src/Logger/Logger.c @@ -0,0 +1,102 @@ +#include "Logger/Logger.h" + +#include <stdarg.h> +#include <stdio.h> +#include <string.h> + +static LogLevel current_level = LOG_LEVEL_INFO; + +static const char *level_name(LogLevel level) { + switch (level) { + case LOG_LEVEL_ERROR: + return "error"; + case LOG_LEVEL_WARNING: + return "warning"; + case LOG_LEVEL_INFO: + return "info"; + case LOG_LEVEL_DEBUG: + return "debug"; + case LOG_LEVEL_SILENT: + return "silent"; + } + return "unknown"; +} + +int logger_is_enabled(LogLevel level) { + return current_level != LOG_LEVEL_SILENT && level <= current_level; +} + +static void log_message(LogLevel level, const char *format, va_list arguments) + YAPSSG_FORMAT_PRINTF(2, 0); + +static void log_message(LogLevel level, const char *format, va_list arguments) { + if (!logger_is_enabled(level)) { + return; + } + + fprintf(stderr, "yapssg: %s: ", level_name(level)); + vfprintf(stderr, format, arguments); + fputc('\n', stderr); + fflush(stderr); +} + +void logger_set_level(LogLevel level) { current_level = level; } + +LogLevel logger_get_level(void) { return current_level; } + +int logger_parse_level(const char *name, LogLevel *level) { + if (strcmp(name, "error") == 0) { + *level = LOG_LEVEL_ERROR; + } else if (strcmp(name, "warning") == 0 || strcmp(name, "warn") == 0) { + *level = LOG_LEVEL_WARNING; + } else if (strcmp(name, "info") == 0) { + *level = LOG_LEVEL_INFO; + } else if (strcmp(name, "debug") == 0) { + *level = LOG_LEVEL_DEBUG; + } else if (strcmp(name, "silent") == 0 || strcmp(name, "quiet") == 0) { + *level = LOG_LEVEL_SILENT; + } else { + return -1; + } + return 0; +} + +void logger_log(LogLevel level, const char *format, ...) { + va_list arguments; + + va_start(arguments, format); + log_message(level, format, arguments); + va_end(arguments); +} + +void logger_error(const char *format, ...) { + va_list arguments; + + va_start(arguments, format); + log_message(LOG_LEVEL_ERROR, format, arguments); + va_end(arguments); +} + +void logger_warning(const char *format, ...) { + va_list arguments; + + va_start(arguments, format); + log_message(LOG_LEVEL_WARNING, format, arguments); + va_end(arguments); +} + +void logger_info(const char *format, ...) { + va_list arguments; + + va_start(arguments, format); + log_message(LOG_LEVEL_INFO, format, arguments); + va_end(arguments); +} + +void logger_debug(const char *format, ...) { + va_list arguments; + + va_start(arguments, format); + log_message(LOG_LEVEL_DEBUG, format, arguments); + va_end(arguments); +} diff --git a/src/Logger/Logger.h b/src/Logger/Logger.h new file mode 100644 index 0000000..f0bef12 --- /dev/null +++ b/src/Logger/Logger.h @@ -0,0 +1,31 @@ +#ifndef YAPSSG_LOGGER_H +#define YAPSSG_LOGGER_H + +#if defined(__GNUC__) || defined(__clang__) +#define YAPSSG_FORMAT_PRINTF(format_index, arguments_index) \ + __attribute__((format(printf, format_index, arguments_index))) +#else +#define YAPSSG_FORMAT_PRINTF(format_index, arguments_index) +#endif + +typedef enum { + LOG_LEVEL_SILENT = -1, + LOG_LEVEL_ERROR, + LOG_LEVEL_WARNING, + LOG_LEVEL_INFO, + LOG_LEVEL_DEBUG +} LogLevel; + +void logger_set_level(LogLevel level); +LogLevel logger_get_level(void); +int logger_is_enabled(LogLevel level); +int logger_parse_level(const char *name, LogLevel *level); + +void logger_log(LogLevel level, const char *format, ...) + YAPSSG_FORMAT_PRINTF(2, 3); +void logger_error(const char *format, ...) YAPSSG_FORMAT_PRINTF(1, 2); +void logger_warning(const char *format, ...) YAPSSG_FORMAT_PRINTF(1, 2); +void logger_info(const char *format, ...) YAPSSG_FORMAT_PRINTF(1, 2); +void logger_debug(const char *format, ...) YAPSSG_FORMAT_PRINTF(1, 2); + +#endif diff --git a/src/Logger/LuaLogger.c b/src/Logger/LuaLogger.c new file mode 100644 index 0000000..5739791 --- /dev/null +++ b/src/Logger/LuaLogger.c @@ -0,0 +1,60 @@ +#include "Logger/LuaLogger.h" + +#include "Logger/Logger.h" + +#include <lauxlib.h> +#include <lua.h> + +static void add_value(lua_State *lua, luaL_Buffer *buffer, int index) { + int type = lua_type(lua, index); + + if (type == LUA_TSTRING || type == LUA_TNUMBER) { + luaL_addstring(buffer, lua_tostring(lua, index)); + } else if (type == LUA_TBOOLEAN) { + luaL_addstring(buffer, lua_toboolean(lua, index) ? "true" : "false"); + } else if (type == LUA_TNIL) { + luaL_addstring(buffer, "nil"); + } else { + luaL_addchar(buffer, '<'); + luaL_addstring(buffer, lua_typename(lua, type)); + luaL_addchar(buffer, '>'); + } +} + +static int log_from_lua(lua_State *lua) { + LogLevel level = (LogLevel)lua_tointeger(lua, lua_upvalueindex(1)); + int argument_count = lua_gettop(lua); + int index; + luaL_Buffer buffer; + + if (!logger_is_enabled(level)) { + return 0; + } + + luaL_buffinit(lua, &buffer); + for (index = 1; index <= argument_count; ++index) { + if (index > 1) { + luaL_addchar(&buffer, '\t'); + } + add_value(lua, &buffer, index); + } + luaL_pushresult(&buffer); + logger_log(level, "%s", lua_tostring(lua, -1)); + lua_pop(lua, 1); + return 0; +} + +static void set_log_function(lua_State *lua, const char *name, LogLevel level) { + lua_pushinteger(lua, (lua_Integer)level); + lua_pushcclosure(lua, log_from_lua, 1); + lua_setfield(lua, -2, name); +} + +void lua_logger_register(lua_State *lua) { + lua_newtable(lua); + set_log_function(lua, "error", LOG_LEVEL_ERROR); + set_log_function(lua, "warn", LOG_LEVEL_WARNING); + set_log_function(lua, "info", LOG_LEVEL_INFO); + set_log_function(lua, "debug", LOG_LEVEL_DEBUG); + lua_setglobal(lua, "log"); +} diff --git a/src/Logger/LuaLogger.h b/src/Logger/LuaLogger.h new file mode 100644 index 0000000..98c9a8f --- /dev/null +++ b/src/Logger/LuaLogger.h @@ -0,0 +1,8 @@ +#ifndef YAPSSG_LUA_LOGGER_H +#define YAPSSG_LUA_LOGGER_H + +#include <lua.h> + +void lua_logger_register(lua_State *lua); + +#endif diff --git a/src/LuaEngine/LuaEngine.c b/src/LuaEngine/LuaEngine.c new file mode 100644 index 0000000..319f408 --- /dev/null +++ b/src/LuaEngine/LuaEngine.c @@ -0,0 +1,273 @@ +#include "LuaEngine/LuaEngine.h" + +#include "DirectoryWalker/DirectoryWalker.h" +#include "Dom/Dom.h" +#include "Logger/Logger.h" +#include "Logger/LuaLogger.h" +#include "Path/Path.h" +#include "Version/Version.h" + +#include <lauxlib.h> +#include <lua.h> +#include <lualib.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> + +typedef enum { SCRIPT_INLINE, SCRIPT_EXTERNAL } ScriptType; + +typedef struct { + char *source; + ScriptType type; + unsigned long line; +} LuaScript; + +typedef struct { + LuaScript *items; + size_t length; + size_t capacity; +} ScriptList; + +static int is_lua_element(const xmlNode *node) { + return node->type == XML_ELEMENT_NODE && + strcasecmp((const char *)node->name, "lua") == 0; +} + +static char *duplicate_string(const char *value) { + size_t length = strlen(value); + char *copy = malloc(length + 1); + + if (copy != NULL) { + memcpy(copy, value, length + 1); + } + return copy; +} + +static int grow_script_list(ScriptList *scripts) { + size_t larger_capacity; + LuaScript *larger_items; + + if (scripts->capacity > SIZE_MAX / (2 * sizeof(*scripts->items))) { + return -1; + } + + larger_capacity = scripts->capacity == 0 ? 4 : scripts->capacity * 2; + larger_items = + realloc(scripts->items, larger_capacity * sizeof(*scripts->items)); + if (larger_items == NULL) { + return -1; + } + + scripts->items = larger_items; + scripts->capacity = larger_capacity; + return 0; +} + +static int append_script(ScriptList *scripts, const char *source, + ScriptType type, unsigned long line) { + LuaScript *script; + + if (scripts->length == scripts->capacity && grow_script_list(scripts) != 0) { + return -1; + } + + script = &scripts->items[scripts->length]; + script->source = duplicate_string(source); + if (script->source == NULL) { + return -1; + } + + script->type = type; + script->line = line; + ++scripts->length; + return 0; +} + +static int collect_script(xmlNode *node, ScriptList *scripts) { + xmlChar *source_path = xmlGetProp(node, BAD_CAST "src"); + xmlChar *code; + int result; + + if (source_path != NULL) { + result = append_script(scripts, (const char *)source_path, SCRIPT_EXTERNAL, + node->line); + xmlFree(source_path); + return result; + } + + code = xmlNodeGetContent(node); + result = append_script(scripts, code == NULL ? "" : (const char *)code, + SCRIPT_INLINE, node->line); + xmlFree(code); + return result; +} + +static int collect_scripts(xmlNode *node, ScriptList *scripts) { + for (; node != NULL; node = node->next) { + if (is_lua_element(node)) { + if (collect_script(node, scripts) != 0) { + return -1; + } + } else if (collect_scripts(node->children, scripts) != 0) { + return -1; + } + } + return 0; +} + +static void free_scripts(ScriptList *scripts) { + size_t index; + + for (index = 0; index < scripts->length; ++index) { + free(scripts->items[index].source); + } + free(scripts->items); +} + +static char *make_chunk_name(const char *input_path, unsigned long line) { + size_t input_length = strlen(input_path); + size_t capacity; + char *chunk_name; + + if (input_length > SIZE_MAX - 32) { + return NULL; + } + capacity = input_length + 32; + chunk_name = malloc(capacity); + + if (chunk_name != NULL) { + snprintf(chunk_name, capacity, "@%s:%lu", input_path, line); + } + return chunk_name; +} + +static int run_script(lua_State *lua, const LuaScript *script, + const char *input_path) { + char *source_path = NULL; + char *chunk_name = NULL; + const char *display_path; + int load_result; + int result; + + if (script->type == SCRIPT_EXTERNAL) { + source_path = path_resolve_from_file(input_path, script->source); + if (source_path == NULL) { + logger_error("unable to allocate Lua source path"); + return -1; + } + display_path = source_path; + load_result = luaL_loadfile(lua, source_path); + } else { + chunk_name = make_chunk_name(input_path, script->line); + if (chunk_name == NULL) { + logger_error("unable to allocate Lua chunk name"); + return -1; + } + display_path = input_path; + load_result = luaL_loadbuffer(lua, script->source, strlen(script->source), + chunk_name); + } + + result = load_result == 0 ? lua_pcall(lua, 0, 0, 0) : load_result; + if (result != 0) { + const char *message = lua_tostring(lua, -1); + + logger_error("Lua error in %s: %s", display_path, + message == NULL ? "unknown error" : message); + lua_pop(lua, 1); + } + + free(chunk_name); + free(source_path); + return result == 0 ? 0 : -1; +} + +static int run_scripts(lua_State *lua, const ScriptList *scripts, + const char *input_path) { + size_t index; + + for (index = 0; index < scripts->length; ++index) { + if (run_script(lua, &scripts->items[index], input_path) != 0) { + return -1; + } + } + return 0; +} + +static void register_yapssg(lua_State *lua) { + lua_newtable(lua); + lua_pushstring(lua, yapssg_version); + lua_setfield(lua, -2, "version"); + lua_setglobal(lua, "yapssg"); +} + +static int replace_document_root(lua_State *lua, htmlDocPtr document, + const char *input_path) { + xmlNodePtr replacement; + xmlNodePtr old_root; + + lua_getglobal(lua, "page"); + if (!lua_istable(lua, -1)) { + logger_error("Lua global 'page' must remain a table: %s", input_path); + lua_pop(lua, 1); + return -1; + } + + replacement = dom_page_from_lua(lua, lua_gettop(lua)); + lua_pop(lua, 1); + if (replacement == NULL || replacement->type != XML_ELEMENT_NODE) { + logger_error("Lua produced an invalid page root: %s", input_path); + xmlFreeNode(replacement); + return -1; + } + + old_root = xmlDocSetRootElement(document, replacement); + xmlFreeNode(old_root); + return 0; +} + +int process_html_document(htmlDocPtr document, const char *input_path) { + xmlNodePtr root = xmlDocGetRootElement(document); + ScriptList scripts = {0}; + lua_State *lua; + int result; + + if (root == NULL) { + logger_error("HTML document has no root element: %s", input_path); + return -1; + } + if (collect_scripts(root, &scripts) != 0) { + logger_error("unable to collect Lua scripts: %s", input_path); + free_scripts(&scripts); + return -1; + } + if (scripts.length == 0) { + free_scripts(&scripts); + return 0; + } + + lua = luaL_newstate(); + if (lua == NULL) { + logger_error("unable to create Lua state"); + free_scripts(&scripts); + return -1; + } + + luaL_openlibs(lua); + register_yapssg(lua); + lua_logger_register(lua); + directory_walker_register(lua, input_path); + dom_push_page(lua, root); + lua_setglobal(lua, "page"); + + result = run_scripts(lua, &scripts, input_path); + if (result == 0) { + result = replace_document_root(lua, document, input_path); + } + + lua_close(lua); + free_scripts(&scripts); + return result; +} diff --git a/src/LuaEngine/LuaEngine.h b/src/LuaEngine/LuaEngine.h new file mode 100644 index 0000000..bd035b9 --- /dev/null +++ b/src/LuaEngine/LuaEngine.h @@ -0,0 +1,8 @@ +#ifndef YAPSSG_LUA_ENGINE_H +#define YAPSSG_LUA_ENGINE_H + +#include <libxml/HTMLparser.h> + +int process_html_document(htmlDocPtr document, const char *input_path); + +#endif diff --git a/src/Main.c b/src/Main.c new file mode 100644 index 0000000..b7c77f1 --- /dev/null +++ b/src/Main.c @@ -0,0 +1,52 @@ +#include "Cli/Cli.h" +#include "FileProcessor/FileProcessor.h" +#include "Logger/Logger.h" +#include "Path/Path.h" + +#include <libxml/parser.h> +#include <stdio.h> +#include <stdlib.h> + +static int run(int argc, char **argv) { + CliOptions options; + CliParseResult parse_result; + char *automatic_output = NULL; + const char *output_path; + int result; + + parse_result = cli_parse(argc, argv, &options); + if (parse_result != CLI_PARSE_OK) { + return parse_result == CLI_PARSE_ERROR ? EXIT_FAILURE : EXIT_SUCCESS; + } + + logger_set_level(options.log_level); + + output_path = options.output_path; + if (output_path == NULL) { + automatic_output = path_make_default_output(options.input_path); + if (automatic_output == NULL) { + logger_error("unable to allocate output path"); + return EXIT_FAILURE; + } + output_path = automatic_output; + } + + if (options.input_type == INPUT_TYPE_DIRECTORY) { + result = process_directory(options.input_path, output_path, + options.overwrite_output); + } else { + result = process_file(options.input_path, output_path); + } + + free(automatic_output); + return result == 0 ? EXIT_SUCCESS : EXIT_FAILURE; +} + +int main(int argc, char **argv) { + int result; + + xmlInitParser(); + result = run(argc, argv); + xmlCleanupParser(); + return result; +} diff --git a/src/Path/Path.c b/src/Path/Path.c new file mode 100644 index 0000000..8a0ab2b --- /dev/null +++ b/src/Path/Path.c @@ -0,0 +1,185 @@ +#define _POSIX_C_SOURCE 200809L +#define _XOPEN_SOURCE 700 + +#include "Path/Path.h" + +#include <stdint.h> +#include <stdlib.h> +#include <string.h> + +static const char output_prefix[] = "yapssg_"; + +static int checked_add(size_t left, size_t right, size_t *sum) { + if (left > SIZE_MAX - right) { + return -1; + } + *sum = left + right; + return 0; +} + +static char *duplicate_string(const char *value) { + size_t length = strlen(value); + char *copy = malloc(length + 1); + + if (copy != NULL) { + memcpy(copy, value, length + 1); + } + return copy; +} + +char *path_join(const char *left, const char *right) { + size_t left_length = strlen(left); + size_t right_length = strlen(right); + int needs_separator = left_length > 0 && left[left_length - 1] != '/'; + size_t path_length; + size_t allocation_size; + char *path; + + if (checked_add(left_length, (size_t)needs_separator, &path_length) != 0 || + checked_add(path_length, right_length, &path_length) != 0 || + checked_add(path_length, 1, &allocation_size) != 0) { + return NULL; + } + + path = malloc(allocation_size); + if (path == NULL) { + return NULL; + } + memcpy(path, left, left_length); + if (needs_separator) { + path[left_length++] = '/'; + } + memcpy(path + left_length, right, right_length + 1); + return path; +} + +char *path_make_default_output(const char *input_path) { + const char *end = input_path + strlen(input_path); + const char *base; + size_t parent_length; + size_t base_length; + size_t prefix_length = sizeof(output_prefix) - 1; + size_t output_length; + size_t allocation_size; + char *output_path; + + while (end > input_path + 1 && end[-1] == '/') { + --end; + } + base = end; + while (base > input_path && base[-1] != '/') { + --base; + } + + parent_length = (size_t)(base - input_path); + base_length = (size_t)(end - base); + if (base_length == 0) { + return NULL; + } + + if (checked_add(parent_length, prefix_length, &output_length) != 0 || + checked_add(output_length, base_length, &output_length) != 0 || + checked_add(output_length, 1, &allocation_size) != 0) { + return NULL; + } + + output_path = malloc(allocation_size); + if (output_path == NULL) { + return NULL; + } + memcpy(output_path, input_path, parent_length); + memcpy(output_path + parent_length, output_prefix, prefix_length); + memcpy(output_path + parent_length + prefix_length, base, base_length); + output_path[parent_length + prefix_length + base_length] = '\0'; + return output_path; +} + +char *path_resolve_new(const char *path) { + char *path_copy = duplicate_string(path); + char *slash; + char *parent; + char *resolved_parent; + char *resolved_path; + const char *base; + size_t length; + + if (path_copy == NULL) { + return NULL; + } + + length = strlen(path_copy); + while (length > 1 && path_copy[length - 1] == '/') { + path_copy[--length] = '\0'; + } + + slash = strrchr(path_copy, '/'); + if (slash == NULL) { + parent = duplicate_string("."); + base = path_copy; + } else if (slash == path_copy) { + parent = duplicate_string("/"); + base = slash + 1; + } else { + *slash = '\0'; + parent = duplicate_string(path_copy); + base = slash + 1; + } + + if (parent == NULL || base[0] == '\0') { + free(parent); + free(path_copy); + return NULL; + } + + resolved_parent = realpath(parent, NULL); + free(parent); + if (resolved_parent == NULL) { + free(path_copy); + return NULL; + } + + resolved_path = path_join(resolved_parent, base); + free(resolved_parent); + free(path_copy); + return resolved_path; +} + +char *path_resolve_from_file(const char *file_path, const char *relative_path) { + const char *slash; + size_t directory_length; + size_t relative_length; + size_t path_length; + size_t allocation_size; + char *path; + + if (relative_path[0] == '/') { + return duplicate_string(relative_path); + } + + slash = strrchr(file_path, '/'); + directory_length = slash == NULL ? 0 : (size_t)(slash - file_path + 1); + relative_length = strlen(relative_path); + if (checked_add(directory_length, relative_length, &path_length) != 0 || + checked_add(path_length, 1, &allocation_size) != 0) { + return NULL; + } + + path = malloc(allocation_size); + if (path == NULL) { + return NULL; + } + + memcpy(path, file_path, directory_length); + memcpy(path + directory_length, relative_path, relative_length + 1); + return path; +} + +int path_is_within(const char *parent, const char *candidate) { + size_t parent_length = strlen(parent); + + if (parent_length == 1 && parent[0] == '/') { + return candidate[0] == '/'; + } + return strncmp(parent, candidate, parent_length) == 0 && + (candidate[parent_length] == '\0' || candidate[parent_length] == '/'); +} diff --git a/src/Path/Path.h b/src/Path/Path.h new file mode 100644 index 0000000..a4ac530 --- /dev/null +++ b/src/Path/Path.h @@ -0,0 +1,10 @@ +#ifndef YAPSSG_PATH_H +#define YAPSSG_PATH_H + +char *path_join(const char *left, const char *right); +char *path_make_default_output(const char *input_path); +char *path_resolve_new(const char *path); +char *path_resolve_from_file(const char *file_path, const char *relative_path); +int path_is_within(const char *parent, const char *candidate); + +#endif diff --git a/src/Version/Version.c b/src/Version/Version.c new file mode 100644 index 0000000..4f4a1ea --- /dev/null +++ b/src/Version/Version.c @@ -0,0 +1,3 @@ +#include "Version/Version.h" + +const char yapssg_version[] = "0.1.0"; diff --git a/src/Version/Version.h b/src/Version/Version.h new file mode 100644 index 0000000..f17b243 --- /dev/null +++ b/src/Version/Version.h @@ -0,0 +1,6 @@ +#ifndef YAPSSG_VERSION_H +#define YAPSSG_VERSION_H + +extern const char yapssg_version[]; + +#endif |
