diff options
Diffstat (limited to 'test')
-rw-r--r-- | test/Makefile | 2 | ||||
-rw-r--r-- | test/cli.c | 827 | ||||
-rw-r--r-- | test/cli.h | 99 | ||||
-rw-r--r-- | test/decode_gif.c | 250 | ||||
-rw-r--r-- | test/nsgif.c | 442 | ||||
-rwxr-xr-x | test/runtest.sh | 60 |
6 files changed, 1400 insertions, 280 deletions
diff --git a/test/Makefile b/test/Makefile index f067d81..a578aef 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,3 +1,3 @@ -DIR_TEST_ITEMS := decode_gif:decode_gif.c +DIR_TEST_ITEMS := nsgif:nsgif.c include $(NSBUILD)/Makefile.subdir diff --git a/test/cli.c b/test/cli.c new file mode 100644 index 0000000..9c095fe --- /dev/null +++ b/test/cli.c @@ -0,0 +1,827 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (C) 2021-2022 Michael Drake <tlsa@netsurf-browser.org> + */ + +/** + * \file + * \brief Command line argument handling. + */ + +#include <ctype.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "cli.h" + +/** + * CLI parsing context. + */ +struct cli_ctx { + const struct cli_table *cli; /**< Client CLI spec. */ + size_t pos_count; /**< The number of positional arguments found. */ + bool no_pos; /**< Have an argument that negates min_positional. */ +}; + +/** + * Check whether a CLI argument type should have a numerical value. + * + * \param[in] type An argument type. + * \return true if the argument needs a numerical value, or false otherwise. + */ +static inline bool cli__arg_is_numerical(enum cli_arg_type type) +{ + return (type != CLI_STRING && type != CLI_BOOL); +} + +/** + * Parse a signed integer value from an argument. + * + * \param[in] str String containing value to parse. + * \param[out] i Pointer to place to store parsed value. + * \param[in,out] pos Current position in str, updated on exit. + * \return true on success, or false otherwise. + */ +static bool cli__parse_value_int( + const char *str, + int64_t *i, + size_t *pos) +{ + long long temp; + char *end = NULL; + + str += *pos; + errno = 0; + temp = strtoll(str, &end, 0); + + if (end == str || errno == ERANGE || + temp > INT64_MAX || temp < INT64_MIN) { + fprintf(stderr, "Failed to parse integer from '%s'\n", str); + return false; + } + + *i = (int64_t)temp; + *pos += (size_t)(end - str); + return true; +} + +/** + * Parse an unsigned integer value from an argument. + * + * \param[in] str String containing value to parse. + * \param[out] u Pointer to place to store parsed value. + * \param[in,out] pos Current position in str, updated on exit. + * \return true on success, or false otherwise. + */ +static bool cli__parse_value_uint( + const char *str, + uint64_t *u, + size_t *pos) +{ + unsigned long long temp; + char *end = NULL; + + str += *pos; + errno = 0; + temp = strtoull(str, &end, 0); + + if (end == str || errno == ERANGE || temp > UINT64_MAX) { + fprintf(stderr, "Failed to parse unsigned from '%s'\n", str); + return false; + } + + *u = (uint64_t)temp; + *pos += (size_t)(end - str); + return true; +} + +/** + * Parse an enum value from an argument. + * + * \param[in] str String containing value to parse. + * \param[out] e Enum details. + * \param[in,out] pos Current position in str, updated on exit. + * \return true on success, or false otherwise. + */ +static bool cli__parse_value_enum( + const char *str, + const struct cli_enum *e, + size_t *pos) +{ + str += *pos; + *pos += strlen(str); + + for (const struct cli_str_val *sv = e->desc; sv->str != NULL; sv++) { + if (strcmp(str, sv->str) == 0) { + *e->e = sv->val; + return true; + } + } + + fprintf(stderr, "ERROR: Unknown enum value '%s'.\n", str); + + return false; +} + +/** + * Parse a string value from an argument. + * + * \param[in] str String containing value to parse. + * \param[out] s Pointer to place to store parsed value. + * \param[in,out] pos Current position in str, updated on exit. + * \return true on success, or false otherwise. + */ +static bool cli__parse_value_string( + const char *str, + const char **s, + size_t *pos) +{ + *s = str + *pos; + *pos += strlen(*s); + return true; +} + +/** + * Parse a value from an argument. + * + * \param[in] entry Client command line interface argument specification. + * \param[in] arg Argument to parse a value from. + * \param[in,out] pos Current position in argument, updated on exit. + * \return true on success, or false otherwise. + */ +static bool cli__parse_value( + const struct cli_table_entry *entry, + const char *arg, + size_t *pos) +{ + switch (entry->t) { + case CLI_CMD: + if (strcmp(arg + *pos, entry->l) == 0) { + *pos += strlen(arg); + return true; + } + return false; + + case CLI_INT: + return cli__parse_value_int(arg, entry->v.i, pos); + + case CLI_UINT: + return cli__parse_value_uint(arg, entry->v.u, pos); + + case CLI_ENUM: + return cli__parse_value_enum(arg, &entry->v.e, pos); + + case CLI_STRING: + return cli__parse_value_string(arg, entry->v.s, pos); + + default: + fprintf(stderr, "Unexpected value for '%s': %s\n", + entry->l, arg); + break; + } + + return false; +} + +/** + * Parse a value from an argument. + * + * \param[in] entry Client command line interface argument specification. + * \param[in] argc Number of command line arguments. + * \param[in] argv String vector containing command line arguments. + * \param[in] arg_pos Current position in argv. + * \param[in,out] pos Current pos in current argument, updated on exit. + * \return true on success, or false otherwise. + */ +static bool cli__parse_argv_value(const struct cli_table_entry *entry, + int argc, const char **argv, + int arg_pos, size_t *pos) +{ + const char *arg = argv[arg_pos]; + + if (arg_pos >= argc) { + fprintf(stderr, "Value not given for '%s'\n", entry->l); + return false; + } + + return cli__parse_value(entry, arg, pos); +} + +/** + * Check whether a CLI argument is a positional value. + * + * \param[in] entry Client command line interface argument specification. + * \return true if the argument is positional, or false otherwise. + */ +static inline bool cli__entry_is_positional(const struct cli_table_entry *entry) +{ + return entry->p; +} + +/** + * Look up a short argument flag. + * + * \param[in] cli Client command line interface specification. + * \param[in] s Argument flag to look up in client CLI spec. + * \return Client CLI spec entry on success, or NULL otherwise. + */ +static const struct cli_table_entry *cli__lookup_short( + const struct cli_table *cli, char s) +{ + for (size_t i = 0; i < cli->count; i++) { + if (cli__entry_is_positional(&cli->entries[i])) { + continue; + } + if (cli->entries[i].s == s) { + return &cli->entries[i]; + } + } + + fprintf(stderr, "Unknown flag: '%c'\n", s); + return NULL; +} + +/** + * Handle an argument with a type that requires a value. + * + * This can handle the value being in the current argument, optionally split by + * a separator, or in the next argument. + * + * \param[in] entry Client command line interface argument specification. + * \param[in] argc Number of command line arguments. + * \param[in] argv String vector containing command line arguments. + * \param[in,out] arg_pos Current position in argv, updated on exit. + * \param[in] pos Current position in current argument string. + * \param[in] sep Name/value separator character, or '\0' if none. + * \return true on success, or false otherwise. + */ +static bool cli__handle_arg_value(const struct cli_table_entry *entry, + int argc, const char **argv, int *arg_pos, size_t pos, char sep) +{ + const char *arg = argv[*arg_pos]; + size_t orig_pos; + bool ret; + + if (arg[pos] == '\0') { + (*arg_pos)++; + pos = 0; + } else if (arg[pos] == sep) { + pos++; + } else if (cli__arg_is_numerical(entry->t) == false) { + fprintf(stderr, "Separator required for non-numerical value\n"); + return false; + } + + if (isspace(argv[*arg_pos][pos])) { + fprintf(stderr, "Unexpected white space in '%s' " + "for argument '%s'\n", + &argv[*arg_pos][pos], entry->l); + return false; + } + + orig_pos = pos; + ret = cli__parse_argv_value(entry, argc, argv, *arg_pos, &pos); + if (ret != true) { + return ret; + } + + if (argv[*arg_pos][pos] != '\0') { + fprintf(stderr, "Invalid value '%s' for argument '%s'\n", + &argv[*arg_pos][orig_pos], entry->l); + return false; + } + + return true; +} + +static inline bool cli__is_negative(const char *arg) +{ + int64_t i; + size_t pos = 0; + + return cli__parse_value_int(arg, &i, &pos) + && pos == strlen(arg) + && i < 0; +} + +/** + * Parse a positional argument according to the given CLI spec entry. + * + * \param[in] ctx Command line interface parsing context. + * \param[in] entry Client command line interface argument specification. + * \param[in] arg Argument to parse. + * \return true on success, or false otherwise. + */ +static bool cli__parse_positional_entry(struct cli_ctx *ctx, + const struct cli_table_entry *entry, + const char *arg) +{ + size_t pos = 0; + bool ret; + + ret = cli__parse_value(entry, arg, &pos); + if (ret != true) { + return ret; + } else if (arg[pos] != '\0') { + fprintf(stderr, "Failed to parse value '%s' for arg '%s'\n", + arg, entry->l); + return false; + } + + ctx->pos_count++; + return true; +} + +/** + * Parse a positional argument. + * + * \param[in] ctx Command line interface parsing context. + * \param[in] arg Argument to parse. + * \return true on success, or false otherwise. + */ +static bool cli__parse_positional(struct cli_ctx *ctx, + const char *arg) +{ + const struct cli_table *cli = ctx->cli; + size_t positional = 0; + + for (size_t i = 0; i < cli->count; i++) { + if (cli__entry_is_positional(&cli->entries[i])) { + if (positional == ctx->pos_count) { + return cli__parse_positional_entry(ctx, + &cli->entries[i], arg); + } + + positional++; + } + } + + fprintf(stderr, "Unexpected positional argument: '%s'\n", arg); + return false; +} + +/** + * Parse a flags argument. + * + * \param[in] ctx Command line interface parsing context. + * \param[in] argc Number of command line arguments. + * \param[in] argv String vector containing command line arguments. + * \param[out] arg_pos Current position in argv, updated on exit. + * \return true on success, or false otherwise. + */ +static bool cli__parse_short(struct cli_ctx *ctx, + int argc, const char **argv, int *arg_pos) +{ + const char *arg = argv[*arg_pos]; + size_t pos = 1; + + if (arg[0] != '-') { + return false; + } + + while (arg[pos] != '\0') { + const struct cli_table_entry *entry; + + entry = cli__lookup_short(ctx->cli, arg[pos]); + if (entry == NULL) { + if (cli__is_negative(argv[pos])) { + return cli__parse_positional(ctx, argv[pos]); + } + return false; + } + + if (entry->no_pos) { + ctx->no_pos = true; + } + + if (entry->t == CLI_BOOL) { + *entry->v.b = true; + } else { + return cli__handle_arg_value(entry, argc, argv, + arg_pos, pos + 1, '\0'); + } + + pos++; + } + + return true; +} + +/** + * Look up a long argument name. + * + * \param[in] cli Client command line interface specification. + * \param[in] arg Argument name to look up in cli spec. + * \param[in,out] pos Current position in arg, updated on exit. + * \return Client CLI spec entry on success, or NULL otherwise. + */ +static const struct cli_table_entry *cli__lookup_long( + const struct cli_table *cli, + const char *arg, + size_t *pos) +{ + arg += *pos; + + for (size_t i = 0; i < cli->count; i++) { + if (cli__entry_is_positional(&cli->entries[i]) == false) { + const char *name = cli->entries[i].l; + size_t name_len = strlen(cli->entries[i].l); + + if (strncmp(name, arg, name_len) == 0) { + if (arg[name_len] != '\0' && + arg[name_len] != '=') { + continue; + } + *pos += name_len; + return &cli->entries[i]; + } + } + } + + fprintf(stderr, "Unknown argument: '%s'\n", arg); + return NULL; +} + +/** + * Parse a long argument. + * + * \param[in] ctx Command line interface parsing context. + * \param[in] argc Number of command line arguments. + * \param[in] argv String vector containing command line arguments. + * \param[out] arg_pos Current position in argv, updated on exit. + * \return true on success, or false otherwise. + */ +static bool cli__parse_long(struct cli_ctx *ctx, + int argc, const char **argv, int *arg_pos) +{ + const struct cli_table_entry *entry; + const char *arg = argv[*arg_pos]; + size_t pos = 2; + + if (arg[0] != '-' || + arg[1] != '-') { + return false; + } + + entry = cli__lookup_long(ctx->cli, arg, &pos); + if (entry == NULL) { + return false; + } + + if (entry->no_pos) { + ctx->no_pos = true; + } + + if (entry->t == CLI_BOOL) { + if (arg[pos] != '\0') { + fprintf(stderr, "Unexpected value for argument '%s'\n", + arg); + return false; + } + *entry->v.b = true; + } else { + bool ret; + + ret = cli__handle_arg_value(entry, argc, argv, + arg_pos, pos, '='); + if (ret != true) { + return ret; + } + } + + return true; +} + +/** + * Get the string to indicate type of value expected for an argument. + * + * \param[in] type The argument type. + * \return String for value type. + */ +static const char *cli__string_from_type(enum cli_arg_type type) +{ + static const char *const strings[] = { + [CLI_BOOL] = "", + [CLI_INT] = "INT", + [CLI_UINT] = "UINT", + [CLI_ENUM] = "ENUM", + [CLI_STRING] = "STRING", + }; + + if (type >= CLI_ARRAY_LEN(strings) || strings[type] == NULL) { + return ""; + } + + return strings[type]; +} + +/** + * Helper to update a maximum adjusted string length if new values is greater. + * + * \param[in] str String to check. + * \param[in] adjustment Amount to modify length of string by (bytes). + * \param[out] len Returns the maximum of existing and this length. + */ +static void cli__max_len(const char *str, size_t adjustment, size_t *len) +{ + size_t str_len = strlen(str) + adjustment; + + if (str_len > *len) { + *len = str_len; + } +} + +/** + * Count up various properties of the client CLI interface specification. + * + * \param[in] cli Client command line interface specification. + * \param[out] count Returns number of non-positional arguments. + * \param[out] pcount Returns number of positional arguments. + * \param[out] max_len Returns max string length of non-positional arguments. + * \param[out] pmax_len Returns max string length of positional arguments. + * \param[out] phas_desc Returns number of positional args with descriptions. + */ +static void cli__count(const struct cli_table *cli, + size_t *count, + size_t *pcount, + size_t *max_len, + size_t *pmax_len, + size_t *phas_desc) +{ + if (count != NULL) *count = 0; + if (pcount != NULL) *pcount = 0; + if (max_len != NULL) *max_len = 0; + if (pmax_len != NULL) *pmax_len = 0; + if (phas_desc != NULL) *phas_desc = 0; + + for (size_t i = 0; i < cli->count; i++) { + const struct cli_table_entry *entry = &cli->entries[i]; + + if (cli__entry_is_positional(entry)) { + if (pcount != NULL) { + (*pcount)++; + } + if (pmax_len != NULL) { + cli__max_len(entry->l, 0, pmax_len); + } + if (phas_desc != NULL) { + (*phas_desc)++; + } + } else { + if (count != NULL) { + (*count)++; + } + if (max_len != NULL) { + const char *type_str; + size_t type_len; + + type_str = cli__string_from_type(entry->t); + type_len = strlen(type_str); + + cli__max_len(entry->l, type_len, max_len); + } + } + } +} + +/* Documented in cli.h */ +bool cli_parse(const struct cli_table *cli, int argc, const char **argv) +{ + struct cli_ctx ctx = { + .cli = cli, + }; + enum { + ARG_PROG_NAME, + ARG_FIRST, + }; + + for (int i = ARG_FIRST; i < argc; i++) { + const char *arg = argv[i]; + bool ret; + + if (arg[0] == '-') { + if (arg[1] == '-') { + ret = cli__parse_long(&ctx, argc, argv, &i); + } else { + ret = cli__parse_short(&ctx, argc, argv, &i); + } + } else { + ret = cli__parse_positional(&ctx, argv[i]); + } + + if (ret != true) { + return ret; + } + } + + if (ctx.no_pos == false && ctx.pos_count < cli->min_positional) { + fprintf(stderr, "Insufficient positional arguments found.\n"); + return false; + } + + return true; +} + +/** + * Get terminal width. + * + * \return terminal width in characters. + */ +static size_t cli__terminal_width(void) +{ + return 80; +} + +/** + * Print a wrapped string, with a given indent. + * + * The indent is assumed to already be applied for the first line of the + * output by the caller. + * + * \param[in] str The string to print. + * \param[in] indent The number of spaces to pad the left margin with. + */ +static void cli__print_wrapping_string(const char *str, size_t indent) +{ + size_t terminal_width = cli__terminal_width(); + size_t avail = (indent > terminal_width) ? 0 : terminal_width - indent; + size_t space = avail; + + while (*str != '\0') { + size_t word_len = strcspn(str, " \n\t"); + if (word_len <= space || space == avail) { + fprintf(stderr, "%*.*s", + (int)word_len, + (int)word_len, str); + str += word_len; + if (word_len <= space) { + space -= word_len; + } + if (space > 0) { + fprintf(stderr, " "); + space--; + } + } else { + fprintf(stderr, "\n%*s", (int)indent, ""); + space = avail; + } + str += strspn(str, " \n\t"); + } +} + +/** + * Print an entry's description, with a given indent. + * + * The indent is assumed to already be applied for the first line of the + * output by the caller. + * + * \param[in] entry The entry to print the description for. + * \param[in] indent The number of spaces to pad the left margin with. + */ +static void cli__print_description(const struct cli_table_entry *entry, + size_t indent) +{ + if (entry->d != NULL) { + cli__print_wrapping_string(entry->d, indent); + } + + fprintf(stderr, "\n"); + + if (entry->t == CLI_ENUM) { + size_t max_len = 0; + + for (const struct cli_str_val *e = entry->v.e.desc; + e->str != NULL; e++) { + size_t len = strlen(e->str); + if (max_len < len) { + max_len = len; + } + } + + fprintf(stderr, "\n"); + + for (const struct cli_str_val *e = entry->v.e.desc; + e->str != NULL; e++) { + fprintf(stderr, " "); + + if (e->d == NULL || e->d[0] == '\0') { + fprintf(stderr, "%s\n", + e->str); + } else { + fprintf(stderr, "%-*s - ", + (int)(max_len), + e->str); + cli__print_wrapping_string(e->d, + 8 + max_len + 3); + fprintf(stderr, "\n"); + } + } + } +} + +/* Documented in cli.h */ +void cli_help(const struct cli_table *cli, const char *prog_name) +{ + size_t count; + size_t pcount; + size_t max_len; + size_t pmax_len; + size_t phas_desc; + size_t required = 0; + enum { + ARG_PROG_NAME, + }; + + cli__count(cli, &count, &pcount, &max_len, &pmax_len, &phas_desc); + + if (cli->d != NULL) { + fprintf(stderr, "\n"); + cli__print_wrapping_string(cli->d, 0); + fprintf(stderr, "\n"); + } + + fprintf(stderr, "\nUsage: %s", prog_name); + + if (pcount > 0) { + for (size_t i = 0; i < cli->count; i++) { + if (cli__entry_is_positional(&cli->entries[i])) { + const char *punctuation = + (required == cli->min_positional) ? + " [" : " "; + + if (cli->entries[i].t == CLI_CMD) { + fprintf(stderr, "%s%s", punctuation, + cli->entries[i].l); + } else { + fprintf(stderr, "%s<%s>", punctuation, + cli->entries[i].l); + } + required++; + } + } + if (required == pcount && required > cli->min_positional) { + fprintf(stderr, "]"); + } + } + + if (count > 0) { + fprintf(stderr, " [options]"); + } + + fprintf(stderr, "\n\n"); + + if (phas_desc > 0) { + fprintf(stderr, "Where:\n\n"); + + for (size_t i = 0; i < cli->count; i++) { + const struct cli_table_entry *entry = &cli->entries[i]; + + if (entry->d == NULL) { + continue; + } + + if (cli__entry_is_positional(entry)) { + fprintf(stderr, " %*.*s ", + (int)pmax_len, + (int)pmax_len, + entry->l); + cli__print_description(entry, pmax_len + 4); + fprintf(stderr, "\n"); + } + } + } + + if (count > 0) { + fprintf(stderr, "Options:\n\n"); + + for (size_t i = 0; i < cli->count; i++) { + const struct cli_table_entry *entry = &cli->entries[i]; + const char *type_str; + size_t type_len; + size_t arg_len; + + if (cli__entry_is_positional(entry)) { + continue; + } + + if (entry->s != '\0') { + fprintf(stderr, " -%c", entry->s); + } else { + fprintf(stderr, " "); + } + + type_str = cli__string_from_type(entry->t); + type_len = strlen(type_str); + arg_len = strlen(entry->l); + + fprintf(stderr, " --%s %s%*.s ", entry->l, type_str, + (int)(max_len - arg_len - type_len), + ""); + cli__print_description(entry, max_len + 11); + fprintf(stderr, "\n"); + } + } +} diff --git a/test/cli.h b/test/cli.h new file mode 100644 index 0000000..ffcd272 --- /dev/null +++ b/test/cli.h @@ -0,0 +1,99 @@ +/* + * SPDX-License-Identifier: ISC + * + * Copyright (C) 2021-2022 Michael Drake <tlsa@netsurf-browser.org> + */ + +/** + * \file + * \brief Command line argument handling API. + */ + +#ifndef _PELTAR_CLI_H_ +#define _PELTAR_CLI_H_ + +#include <stdint.h> +#include <stdbool.h> + +/** + * Helper to get element count for an array, + * + * \param[in] _a Array to get number of elements for. + */ +#define CLI_ARRAY_LEN(_a) ((sizeof(_a))/(sizeof(*(_a)))) + +/** + * CLI argument type. + */ +enum cli_arg_type { + CLI_CMD, /**< A sub-command. Must match long argument name. */ + CLI_BOOL, /**< Has no value; presence of flag indicates true. */ + CLI_INT, /**< Has signed integer value. */ + CLI_UINT, /**< Has unsigned integer value. */ + CLI_ENUM, /**< Has enumeration value. */ + CLI_STRING, /**< Has string value. */ +}; + +/** Enum value descriptor. */ +struct cli_str_val { + const char *str; /**< String for the enum value name. */ + int64_t val; /**< The value for this string. */ + const char *d; /**< Description of this value for help output. */ +}; + +/** Enum data. */ +struct cli_enum { + const struct cli_str_val *desc; /**< Array describing enum values. */ + int64_t *e; /**< Location to store \ref CLI_ENUM value. */ +}; + +/** + * Client description for a command line argument. + */ +struct cli_table_entry { + const char *l; /**< Long argument name. */ + const char s; /**< Short flag name. (Non-positional arguments.) */ + bool p; /**< Whether the argument is a positional argument. */ + bool no_pos; /**< When present, no positional arguments are required. */ + enum cli_arg_type t; /**< Argument type. */ + union { + bool *b; /**< Location to store \ref CLI_BOOL value. */ + int64_t *i; /**< Location to store \ref CLI_INT value. */ + uint64_t *u; /**< Location to store \ref CLI_UINT value. */ + const char **s; /**< Location to store \ref CLI_STRING value. */ + struct cli_enum e; /**< \ref CLI_ENUM value details. */ + } v; /**< Where to store type-specific values. */ + const char *d; /**< Description of this argument for help output. */ +}; + +/** + * Client command line interface specification. + */ +struct cli_table { + const struct cli_table_entry *entries; + size_t count; + size_t min_positional; + const char *d; /**< Description of this application for help output. */ +}; + +/** + * Parse the command line arguments. + * + * \param[in] cli Client command line interface specification. + * \param[in] argc Number of command line arguments. + * \param[in] argv String vector containing command line arguments. + * \return true on success, false on error. + */ +bool cli_parse(const struct cli_table *cli, int argc, const char **argv); + +/** + * Print usage and help output. + * + * Note: Assumes non-Unicode. (One byte per character.) + * + * \param[in] cli Client command line interface specification. + * \param[in] prog_name Program name. + */ +void cli_help(const struct cli_table *cli, const char *prog_name); + +#endif diff --git a/test/decode_gif.c b/test/decode_gif.c deleted file mode 100644 index 619be29..0000000 --- a/test/decode_gif.c +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright 2008 Sean Fox <dyntryx@gmail.com> - * Copyright 2008 James Bursa <james@netsurf-browser.org> - * - * This file is part of NetSurf's libnsgif, http://www.netsurf-browser.org/ - * Licenced under the MIT License, - * http://www.opensource.org/licenses/mit-license.php - */ - -#include <assert.h> -#include <errno.h> -#include <stdbool.h> -#include <stdlib.h> -#include <stdio.h> -#include <string.h> -#include <sys/stat.h> - -#include "../include/libnsgif.h" - -#define BYTES_PER_PIXEL 4 -#define MAX_IMAGE_BYTES (48 * 1024 * 1024) - - -static void *bitmap_create(int width, int height) -{ - /* ensure a stupidly large bitmap is not created */ - if (((long long)width * (long long)height) > (MAX_IMAGE_BYTES/BYTES_PER_PIXEL)) { - return NULL; - } - return calloc(width * height, BYTES_PER_PIXEL); -} - - -static void bitmap_set_opaque(void *bitmap, bool opaque) -{ - (void) opaque; /* unused */ - (void) bitmap; /* unused */ - assert(bitmap); -} - - -static bool bitmap_test_opaque(void *bitmap) -{ - (void) bitmap; /* unused */ - assert(bitmap); - return false; -} - - -static unsigned char *bitmap_get_buffer(void *bitmap) -{ - assert(bitmap); - return bitmap; -} - - -static void bitmap_destroy(void *bitmap) -{ - assert(bitmap); - free(bitmap); -} - - -static void bitmap_modified(void *bitmap) -{ - (void) bitmap; /* unused */ - assert(bitmap); - return; -} - -static unsigned char *load_file(const char *path, size_t *data_size) -{ - FILE *fd; - struct stat sb; - unsigned char *buffer; - size_t size; - size_t n; - - fd = fopen(path, "rb"); - if (!fd) { - perror(path); - exit(EXIT_FAILURE); - } - - if (stat(path, &sb)) { - perror(path); - exit(EXIT_FAILURE); - } - size = sb.st_size; - - buffer = malloc(size); - if (!buffer) { - fprintf(stderr, "Unable to allocate %lld bytes\n", - (long long) size); - exit(EXIT_FAILURE); - } - - n = fread(buffer, 1, size, fd); - if (n != size) { - perror(path); - exit(EXIT_FAILURE); - } - - fclose(fd); - - *data_size = size; - return buffer; -} - - -static void warning(const char *context, gif_result code) -{ - fprintf(stderr, "%s failed: ", context); - switch (code) - { - case GIF_INSUFFICIENT_FRAME_DATA: - fprintf(stderr, "GIF_INSUFFICIENT_FRAME_DATA"); - break; - case GIF_FRAME_DATA_ERROR: - fprintf(stderr, "GIF_FRAME_DATA_ERROR"); - break; - case GIF_INSUFFICIENT_DATA: - fprintf(stderr, "GIF_INSUFFICIENT_DATA"); - break; - case GIF_DATA_ERROR: - fprintf(stderr, "GIF_DATA_ERROR"); - break; - case GIF_INSUFFICIENT_MEMORY: - fprintf(stderr, "GIF_INSUFFICIENT_MEMORY"); - break; - default: - fprintf(stderr, "unknown code %i", code); - break; - } - fprintf(stderr, "\n"); -} - -static void write_ppm(FILE* fh, const char *name, gif_animation *gif, - bool no_write) -{ - unsigned int i; - gif_result code; - - if (!no_write) { - fprintf(fh, "P3\n"); - fprintf(fh, "# %s\n", name); - fprintf(fh, "# width %u \n", gif->width); - fprintf(fh, "# height %u \n", gif->height); - fprintf(fh, "# frame_count %u \n", gif->frame_count); - fprintf(fh, "# frame_count_partial %u \n", gif->frame_count_partial); - fprintf(fh, "# loop_count %u \n", gif->loop_count); - fprintf(fh, "%u %u 256\n", gif->width, gif->height * gif->frame_count); - } - - /* decode the frames */ - for (i = 0; i != gif->frame_count; i++) { - unsigned int row, col; - unsigned char *image; - - code = gif_decode_frame(gif, i); - if (code != GIF_OK) - warning("gif_decode_frame", code); - - if (!no_write) { - fprintf(fh, "# frame %u:\n", i); - image = (unsigned char *) gif->frame_image; - for (row = 0; row != gif->height; row++) { - for (col = 0; col != gif->width; col++) { - size_t z = (row * gif->width + col) * 4; - fprintf(fh, "%u %u %u ", - (unsigned char) image[z], - (unsigned char) image[z + 1], - (unsigned char) image[z + 2]); - } - fprintf(fh, "\n"); - } - } - } - -} - -int main(int argc, char *argv[]) -{ - gif_bitmap_callback_vt bitmap_callbacks = { - bitmap_create, - bitmap_destroy, - bitmap_get_buffer, - bitmap_set_opaque, - bitmap_test_opaque, - bitmap_modified - }; - gif_animation gif; - size_t size; - gif_result code; - unsigned char *data; - FILE *outf = stdout; - bool no_write = false; - - if (argc < 2) { - fprintf(stderr, "Usage: %s image.gif [out]\n", argv[0]); - fprintf(stderr, "\n"); - fprintf(stderr, "If [out] is NOWRITE, the gif will be docoded " - "but not output.\n"); - fprintf(stderr, "Otherwise [out] is an output filename.\n"); - fprintf(stderr, "When [out] is unset, output is to stdout.\n"); - - return 1; - } - - if (argc > 2) { - if (strcmp(argv[2], "NOWRITE") == 0) { - no_write = true; - } else { - outf = fopen(argv[2], "w+"); - if (outf == NULL) { - fprintf(stderr, "Unable to open %s for writing\n", argv[2]); - return 2; - } - } - } - - /* create our gif animation */ - gif_create(&gif, &bitmap_callbacks); - - /* load file into memory */ - data = load_file(argv[1], &size); - - /* begin decoding */ - do { - code = gif_initialise(&gif, size, data); - if (code != GIF_OK && code != GIF_WORKING) { - warning("gif_initialise", code); - gif_finalise(&gif); - free(data); - return 1; - } - } while (code != GIF_OK); - - write_ppm(outf, argv[1], &gif, no_write); - - if (argc > 2 && !no_write) { - fclose(outf); - } - - /* clean up */ - gif_finalise(&gif); - free(data); - - return 0; -} diff --git a/test/nsgif.c b/test/nsgif.c new file mode 100644 index 0000000..29341e7 --- /dev/null +++ b/test/nsgif.c @@ -0,0 +1,442 @@ +/* + * Copyright 2008 Sean Fox <dyntryx@gmail.com> + * Copyright 2008 James Bursa <james@netsurf-browser.org> + * Copyright 2022 Michael Drake <tlsa@netsurf-browser.org> + * + * This file is part of NetSurf's libnsgif, http://www.netsurf-browser.org/ + * Licenced under the MIT License, + * http://www.opensource.org/licenses/mit-license.php + */ + +#include <assert.h> +#include <errno.h> +#include <stdbool.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> + +#include "../include/nsgif.h" + +#include "cli.h" +#include "cli.c" + +#define STR_VAL(_S) STR(_S) +#define STR(_S) #_S + +#define BYTES_PER_PIXEL 4 + +static struct nsgif_options { + const char *file; + const char *ppm; + uint64_t loops; + bool palette; + bool version; + bool info; + bool help; +} nsgif_options; + +static const struct cli_table_entry cli_entries[] = { + { + .s = 'h', + .l = "help", + .t = CLI_BOOL, + .no_pos = true, + .v.b = &nsgif_options.help, + .d = "Print this text.", + }, + { + .s = 'i', + .l = "info", + .t = CLI_BOOL, + .v.b = &nsgif_options.info, + .d = "Dump GIF info to stdout." + }, + { + .s = 'l', + .l = "loops", + .t = CLI_UINT, + .v.u = &nsgif_options.loops, + .d = "Loop through decoding all frames N times. " + "The default is 1." + }, + { + .s = 'm', + .l = "ppm", + .t = CLI_STRING, + .v.s = &nsgif_options.ppm, + .d = "Convert frames to PPM image at given path." + }, + { + .s = 'p', + .l = "palette", + .t = CLI_BOOL, + .v.b = &nsgif_options.palette, + .d = "Save palette images." + }, + { + .s = 'V', + .l = "version", + .t = CLI_BOOL, + .no_pos = true, + .v.b = &nsgif_options.version, + .d = "Print version number." + }, + { + .p = true, + .l = "FILE", + .t = CLI_STRING, + .v.s = &nsgif_options.file, + .d = "Path to GIF file to load." + }, +}; + +const struct cli_table cli = { + .entries = cli_entries, + .count = (sizeof(cli_entries))/(sizeof(*cli_entries)), + .min_positional = 1, + .d = "NSGIF - A utility for inspecting and decoding GIFs with libnsgif", +}; + +static void *bitmap_create(int width, int height) +{ + /* Ensure a stupidly large bitmap is not created */ + if (width > 4096 || height > 4096) { + return NULL; + } + + return calloc(width * height, BYTES_PER_PIXEL); +} + +static unsigned char *bitmap_get_buffer(void *bitmap) +{ + return bitmap; +} + +static void bitmap_destroy(void *bitmap) +{ + free(bitmap); +} + +static uint8_t *load_file(const char *path, size_t *data_size) +{ + FILE *fd; + struct stat sb; + unsigned char *buffer; + size_t size; + size_t n; + + fd = fopen(path, "rb"); + if (!fd) { + perror(path); + exit(EXIT_FAILURE); + } + + if (stat(path, &sb)) { + perror(path); + exit(EXIT_FAILURE); + } + size = sb.st_size; + + buffer = malloc(size); + if (!buffer) { + fprintf(stderr, "Unable to allocate %lld bytes\n", + (long long) size); + exit(EXIT_FAILURE); + } + + n = fread(buffer, 1, size, fd); + if (n != size) { + perror(path); + exit(EXIT_FAILURE); + } + + fclose(fd); + + *data_size = size; + return buffer; +} + +static void warning(const char *context, nsgif_error err) +{ + fprintf(stderr, "%s: %s\n", context, nsgif_strerror(err)); +} + +static void print_gif_info(const nsgif_info_t *info) +{ + const uint8_t *bg = (uint8_t *) &info->background; + + fprintf(stdout, "gif:\n"); + fprintf(stdout, " width: %"PRIu32"\n", info->width); + fprintf(stdout, " height: %"PRIu32"\n", info->height); + fprintf(stdout, " max-loops: %"PRIu32"\n", info->loop_max); + fprintf(stdout, " frame-count: %"PRIu32"\n", info->frame_count); + fprintf(stdout, " global palette: %s\n", info->global_palette ? "yes" : "no"); + fprintf(stdout, " background:\n"); + fprintf(stdout, " red: 0x%"PRIx8"\n", bg[0]); + fprintf(stdout, " green: 0x%"PRIx8"\n", bg[1]); + fprintf(stdout, " blue: 0x%"PRIx8"\n", bg[2]); + fprintf(stdout, " frames:\n"); +} + +static void print_gif_frame_info(const nsgif_frame_info_t *info, uint32_t i) +{ + const char *disposal = nsgif_str_disposal(info->disposal); + + fprintf(stdout, " - frame: %"PRIu32"\n", i); + fprintf(stdout, " local palette: %s\n", info->local_palette ? "yes" : "no"); + fprintf(stdout, " disposal-method: %s\n", disposal); + fprintf(stdout, " transparency: %s\n", info->transparency ? "yes" : "no"); + fprintf(stdout, " interlaced: %s\n", info->interlaced ? "yes" : "no"); + fprintf(stdout, " display: %s\n", info->display ? "yes" : "no"); + fprintf(stdout, " delay: %"PRIu32"\n", info->delay); + fprintf(stdout, " rect:\n"); + fprintf(stdout, " x: %"PRIu32"\n", info->rect.x0); + fprintf(stdout, " y: %"PRIu32"\n", info->rect.y0); + fprintf(stdout, " w: %"PRIu32"\n", info->rect.x1 - info->rect.x0); + fprintf(stdout, " h: %"PRIu32"\n", info->rect.y1 - info->rect.y0); +} + +static bool save_palette( + const char *img_filename, + const char *palette_filename, + const uint32_t palette[NSGIF_MAX_COLOURS], + size_t used_entries) +{ + enum { + SIZE = 32, + COUNT = 16, + }; + FILE *f; + int size = COUNT * SIZE + 1; + + f = fopen(palette_filename, "w+"); + if (f == NULL) { + fprintf(stderr, "Unable to open %s for writing\n", + palette_filename); + return false; + } + + fprintf(f, "P3\n"); + fprintf(f, "# %s: %s\n", img_filename, palette_filename); + fprintf(f, "# Colour count: %zu\n", used_entries); + fprintf(f, "%u %u 256\n", size, size); + + for (int y = 0; y < size; y++) { + for (int x = 0; x < size; x++) { + if (x % SIZE == 0 || y % SIZE == 0) { + fprintf(f, "0 0 0 "); + } else { + size_t offset = y / SIZE * COUNT + x / SIZE; + uint8_t *entry = (uint8_t *)&palette[offset]; + + fprintf(f, "%u %u %u ", + entry[0], + entry[1], + entry[2]); + } + } + + fprintf(f, "\n"); + } + + fclose(f); + + return true; +} + +static bool save_global_palette(const nsgif_t *gif) +{ + uint32_t table[NSGIF_MAX_COLOURS]; + size_t entries; + + nsgif_global_palette(gif, table, &entries); + + return save_palette(nsgif_options.file, "global-palette.ppm", + table, entries); +} + +static bool save_local_palette(const nsgif_t *gif, uint32_t frame) +{ + static uint32_t table[NSGIF_MAX_COLOURS]; + char filename[64]; + size_t entries; + + snprintf(filename, sizeof(filename), "local-palette-%"PRIu32".ppm", + frame); + + if (!nsgif_local_palette(gif, frame, table, &entries)) { + return false; + } + + return save_palette(nsgif_options.file, filename, table, entries); +} + +static void decode(FILE* ppm, const char *name, nsgif_t *gif, bool first) +{ + nsgif_error err; + uint32_t frame_prev = 0; + const nsgif_info_t *info; + + info = nsgif_get_info(gif); + + if (first && ppm != NULL) { + fprintf(ppm, "P3\n"); + fprintf(ppm, "# %s\n", name); + fprintf(ppm, "# width %u \n", info->width); + fprintf(ppm, "# height %u \n", info->height); + fprintf(ppm, "# frame_count %u \n", info->frame_count); + fprintf(ppm, "# loop_max %u \n", info->loop_max); + fprintf(ppm, "%u %u 256\n", info->width, + info->height * info->frame_count); + } + + if (first && nsgif_options.info) { + print_gif_info(info); + } + if (first && nsgif_options.palette && info->global_palette) { + save_global_palette(gif); + } + + /* decode the frames */ + while (true) { + nsgif_bitmap_t *bitmap; + const uint8_t *image; + uint32_t frame_new; + uint32_t delay_cs; + nsgif_rect_t area; + + err = nsgif_frame_prepare(gif, &area, + &delay_cs, &frame_new); + if (err != NSGIF_OK) { + warning("nsgif_frame_prepare", err); + return; + } + + if (frame_new < frame_prev) { + /* Must be an animation that loops. We only care about + * decoding each frame once in this utility. */ + return; + } + frame_prev = frame_new; + + if (first && nsgif_options.info) { + const nsgif_frame_info_t *f_info; + + f_info = nsgif_get_frame_info(gif, frame_new); + if (f_info != NULL) { + print_gif_frame_info(f_info, frame_new); + } + } + if (first && nsgif_options.palette) { + save_local_palette(gif, frame_new); + } + + err = nsgif_frame_decode(gif, frame_new, &bitmap); + if (err != NSGIF_OK) { + fprintf(stderr, "Frame %"PRIu32": " + "nsgif_decode_frame failed: %s\n", + frame_new, nsgif_strerror(err)); + /* Continue decoding the rest of the frames. */ + + } else if (first && ppm != NULL) { + fprintf(ppm, "# frame %u:\n", frame_new); + image = (const uint8_t *) bitmap; + for (uint32_t y = 0; y != info->height; y++) { + for (uint32_t x = 0; x != info->width; x++) { + size_t z = (y * info->width + x) * 4; + fprintf(ppm, "%u %u %u ", + image[z], + image[z + 1], + image[z + 2]); + } + fprintf(ppm, "\n"); + } + } + + if (delay_cs == NSGIF_INFINITE) { + /** This frame is the last. */ + return; + } + } +} + +int main(int argc, char *argv[]) +{ + const nsgif_bitmap_cb_vt bitmap_callbacks = { + .create = bitmap_create, + .destroy = bitmap_destroy, + .get_buffer = bitmap_get_buffer, + }; + size_t size; + nsgif_t *gif; + uint8_t *data; + nsgif_error err; + FILE *ppm = NULL; + + /* Override default options with any command line args */ + if (!cli_parse(&cli, argc, (void *)argv)) { + cli_help(&cli, argv[0]); + return EXIT_FAILURE; + } + + if (nsgif_options.help) { + cli_help(&cli, argv[0]); + return EXIT_SUCCESS; + } + + if (nsgif_options.version) { + printf("%s %s\n", STR_VAL(NSGIF_NAME), STR_VAL(NSGIF_VERSION)); + return EXIT_SUCCESS; + } + + if (nsgif_options.ppm != NULL) { + ppm = fopen(nsgif_options.ppm, "w+"); + if (ppm == NULL) { + fprintf(stderr, "Unable to open %s for writing\n", + nsgif_options.ppm); + return EXIT_FAILURE; + } + } + + /* create our gif animation */ + err = nsgif_create(&bitmap_callbacks, NSGIF_BITMAP_FMT_R8G8B8A8, &gif); + if (err != NSGIF_OK) { + warning("nsgif_create", err); + return EXIT_FAILURE; + } + + /* load file into memory */ + data = load_file(nsgif_options.file, &size); + + /* Scan the raw data */ + err = nsgif_data_scan(gif, size, data); + if (err != NSGIF_OK) { + /* Not fatal; some GIFs are nasty. Can still try to decode + * any frames that were decoded successfully. */ + warning("nsgif_data_scan", err); + } + + nsgif_data_complete(gif); + + if (nsgif_options.loops == 0) { + nsgif_options.loops = 1; + } + + for (uint64_t i = 0; i < nsgif_options.loops; i++) { + decode(ppm, nsgif_options.file, gif, i == 0); + + /* We want to ignore any loop limit in the GIF. */ + nsgif_reset(gif); + } + + if (ppm != NULL) { + fclose(ppm); + } + + /* clean up */ + nsgif_destroy(gif); + free(data); + + return 0; +} diff --git a/test/runtest.sh b/test/runtest.sh index fd5a32b..fd84847 100755 --- a/test/runtest.sh +++ b/test/runtest.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/sh # run test images through libnsgif and count results @@ -20,25 +20,25 @@ GIFTESTS="${GIFTESTS} test/ns-afl-gif/*.gif" gifdecode() { - OUTF=$(basename ${1} .gif) - CMPF=$(dirname ${1})/${OUTF}.ppm - echo "GIF:${1}" >> ${TEST_LOG} - ${TEST_PATH}/test_decode_gif ${1} ${TEST_OUT}/${OUTF}.ppm 2>> ${TEST_LOG} - ECODE=$? - - echo "Exit code:${ECODE}" >> ${TEST_LOG} - if [ "${ECODE}" -gt 0 ];then - return ${ECODE} - fi - - if [ -f "${CMPF}" ]; then - cmp ${CMPF} ${TEST_OUT}/${OUTF}.ppm >> ${TEST_LOG} 2>> ${TEST_LOG} - if [ "$?" -ne 0 ]; then - return 128 + OUTF=$(basename ${1} .gif) + CMPF=$(dirname ${1})/${OUTF}.ppm + echo "GIF:${1}" >> ${TEST_LOG} + ${TEST_PATH}/test_nsgif ${1} --ppm ${TEST_OUT}/${OUTF}.ppm 2>> ${TEST_LOG} + ECODE=$? + + echo "Exit code:${ECODE}" >> ${TEST_LOG} + if [ "${ECODE}" -gt 0 ];then + return ${ECODE} fi - fi - return 0 + if [ -f "${CMPF}" ]; then + cmp ${CMPF} ${TEST_OUT}/${OUTF}.ppm >> ${TEST_LOG} 2>> ${TEST_LOG} + if [ "$?" -ne 0 ]; then + return 128 + fi + fi + + return 0 } GIFTESTTOTC=0 @@ -49,25 +49,27 @@ GIFTESTERRC=0 echo "Testing GIF decode" for GIF in $(ls ${GIFTESTS});do - GIFTESTTOTC=$((GIFTESTTOTC+1)) - gifdecode ${GIF} - ECODE=$? - if [ "${ECODE}" -gt 127 ];then - GIFTESTERRC=$((GIFTESTERRC+1)) - else - if [ "${ECODE}" -gt 0 ];then - GIFTESTFAILC=$((GIFTESTFAILC+1)) + GIFTESTTOTC=$((GIFTESTTOTC+1)) + #echo "${GIF}" + gifdecode ${GIF} + ECODE=$? + if [ "${ECODE}" -gt 127 ];then + GIFTESTERRC=$((GIFTESTERRC+1)) + echo "Error ${GIF}" else - GIFTESTPASSC=$((GIFTESTPASSC+1)) + if [ "${ECODE}" -gt 0 ];then + GIFTESTFAILC=$((GIFTESTFAILC+1)) + else + GIFTESTPASSC=$((GIFTESTPASSC+1)) + fi fi - fi done echo "Tests:${GIFTESTTOTC} Pass:${GIFTESTPASSC} Fail:${GIFTESTFAILC} Error:${GIFTESTERRC}" # exit code if [ "${GIFTESTERRC}" -gt 0 ]; then - exit 1 + exit 1 fi exit 0 |