#!/usr/bin/python # Copyright (C) 2021 Daiki Ueno # This file is part of GnuTLS. # GnuTLS is free software: you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # GnuTLS is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see # . from typing import Mapping, MutableMapping, MutableSequence, Sequence from typing import TextIO, Union import io import os.path import jsonopts import sys INDENT = ' ' def get_aliases(options) -> Mapping[str, Sequence[str]]: aliases: MutableMapping[str, MutableSequence[str]] = dict() for option in options: long_opt = option['long-option'] key = option.get('aliases') if key: val = aliases.get(key, list()) val.append(long_opt) aliases[key] = val return aliases def get_chars(options) -> Mapping[str, Union[str, int]]: chars = dict() chars_counter = 1 short_opts: MutableMapping[str, str] = dict() for option in options: long_opt = option['long-option'] short_opt = option.get('short-option') # If the short option is already taken, do not register twice if short_opt and short_opt in short_opts: print((f'short option {short_opt} for {long_opt} is already ' f'taken by {short_opts[short_opt]}'), file=sys.stderr) short_opt = None if short_opt: chars[long_opt] = short_opt short_opts[short_opt] = long_opt else: chars[long_opt] = chars_counter chars_counter += 1 disable_prefix = option.get('disable-prefix') if disable_prefix: chars[f'{disable_prefix}{long_opt}'] = chars_counter chars_counter += 1 return chars def mangle(name: str) -> str: return ''.join([c if c in 'abcdefghijklmnopqrstuvwxyz0123456789_' else '_' for c in name.lower()]) def format_long_opt(c: Union[str, int], long_opt: str, has_arg: str) -> str: if isinstance(c, str): return f"{INDENT}{{ \"{long_opt}\", {has_arg}, 0, '{c}' }},\n" else: return f'{INDENT}{{ "{long_opt}", {has_arg}, 0, CHAR_MAX + {c} }},\n' def format_switch_case(c: Union[str, int], long_opt: str) -> str: if isinstance(c, str): return f"{INDENT*3}case '{c}':\n" else: return f'{INDENT*3}case CHAR_MAX + {c}: /* --{long_opt} */\n' def gen_c(meta: Mapping[str, str], options: Sequence[Mapping[str, str]], aliases: Mapping[str, Sequence[str]], usage: str, outfile: TextIO): long_opts = io.StringIO() short_opts = list() switch_cases = io.StringIO() enable_statements = io.StringIO() constraint_statements = io.StringIO() has_list_arg = False has_number_arg = False chars = get_chars(options) prog_name = meta['prog-name'] struct_name = f'{mangle(prog_name)}_opts' global_name = f'{mangle(prog_name)}Options' switch_cases.write(f"{INDENT*3}case '\\0': /* Long option. */\n") switch_cases.write(f'{INDENT*4}break;\n') for option in options: long_opt = option['long-option'] arg_type = option.get('arg-type') lower_opt = mangle(long_opt) upper_opt = lower_opt.upper() # aliases are handled differently if 'aliases' in option: continue if arg_type: if 'arg-optional' in option: has_arg = 'optional_argument' else: has_arg = 'required_argument' else: has_arg = 'no_argument' c = chars[long_opt] if isinstance(c, str): if arg_type: short_opts.append(c + ':') else: short_opts.append(c) long_opts.write(format_long_opt(c, long_opt, has_arg)) switch_cases.write(format_switch_case(c, long_opt)) for alias in aliases.get(long_opt, list()): c = chars[alias] long_opts.write(format_long_opt(c, alias, has_arg)) switch_cases.write(format_switch_case(c, alias)) switch_cases.write(f'{INDENT*4}opts->present.{lower_opt} = true;\n') if arg_type: if 'stack-arg' in option: has_list_arg = True switch_cases.write(( f'{INDENT*4}append_to_list (&opts->list.{lower_opt}, ' f'"{long_opt}", optarg);\n' )) else: switch_cases.write( f'{INDENT*4}opts->arg.{lower_opt} = optarg;\n' ) if arg_type == 'number': has_number_arg = True switch_cases.write(( f'{INDENT*4}opts->value.{lower_opt} = ' 'parse_number(optarg);\n' )) if 'enabled' in option or 'disabled' in option: switch_cases.write( f'{INDENT*4}opts->enabled.{lower_opt} = true;\n' ) switch_cases.write(f'{INDENT*4}break;\n') if 'enabled' in option: enable_statements.write( f'{INDENT}opts->enabled.{lower_opt} = true;\n' ) disable_prefix = option.get('disable-prefix') if disable_prefix: disable_opt = f'{disable_prefix}{long_opt}' c = chars[disable_opt] long_opts.write(format_long_opt(c, disable_opt, has_arg)) switch_cases.write(format_switch_case(c, disable_opt)) switch_cases.write( f'{INDENT*4}opts->present.{lower_opt} = true;\n' ) switch_cases.write( f'{INDENT*4}opts->enabled.{lower_opt} = false;\n' ) switch_cases.write(f'{INDENT*4}break;\n') conflict_opts = option.get('conflicts', '').split() for conflict_opt in conflict_opts: constraint_statements.write(f'''\ {INDENT}if (HAVE_OPT({upper_opt}) && HAVE_OPT({mangle(conflict_opt).upper()})) {INDENT*2}{{ {INDENT*3}error (EXIT_FAILURE, 0, "the '%s' and '%s' options conflict", {INDENT*3} "{long_opt}", "{mangle(conflict_opt)}"); {INDENT*2}}} ''') require_opts = option.get('requires', '').split() for require_opt in require_opts: constraint_statements.write(f'''\ {INDENT}if (HAVE_OPT({upper_opt}) && !HAVE_OPT({mangle(require_opt).upper()})) {INDENT*2}{{ {INDENT*3}error (EXIT_FAILURE, 0, "%s option requires the %s options", {INDENT*3} "{long_opt}", "{mangle(require_opt)}"); {INDENT*2}}} ''') arg_min = option.get('arg-min') if arg_min: constraint_statements.write(f'''\ {INDENT}if (HAVE_OPT({upper_opt}) && OPT_VALUE_{upper_opt} < {int(arg_min)}) {INDENT*2}{{ {INDENT*3}error (EXIT_FAILURE, 0, "%s option value %d is out of range.", {INDENT*3} "{long_opt}", opts->value.{lower_opt}); {INDENT*2}}} ''') arg_max = option.get('arg-max') if arg_max: constraint_statements.write(f'''\ {INDENT}if (HAVE_OPT({upper_opt}) && OPT_VALUE_{upper_opt} > {int(arg_max)}) {INDENT*2}{{ {INDENT*3}error (EXIT_FAILURE, 0, "%s option value %d is out of range", {INDENT*3} "{long_opt}", opts->value.{lower_opt}); {INDENT*2}}} ''') long_opts.write(f'{INDENT}{{ 0, 0, 0, 0 }}\n') switch_cases.write(f'{INDENT*3}default:\n') switch_cases.write(f'{INDENT*4}usage (stderr, EXIT_FAILURE);\n') switch_cases.write(f'{INDENT*4}break;\n') argument = meta.get('argument') if argument: if argument.startswith('[') and argument.endswith(']'): argument = argument[1:-1] argument_statement = '' else: argument_statement = f'''\ {INDENT}if (optind == argc) {INDENT*2}{{ {INDENT*3}error (EXIT_FAILURE, 0, "Command line arguments required"); {INDENT*2}}} ''' else: argument_statement = f'''\ {INDENT}if (optind < argc) {INDENT*2}{{ {INDENT*3}error (EXIT_FAILURE, 0, "Command line arguments are not allowed."); {INDENT*2}}} ''' short_opts_concatenated = ''.join(sorted(short_opts)) usage_stringified = '\n'.join([ f'{INDENT*2}"{line}\\n"' for line in usage.split('\n') ]) brief_version = jsonopts.version(meta, 'v') version = jsonopts.version(meta, 'c') full_version = jsonopts.version(meta, 'n') brief_version_stringified = '\n'.join([ f'{INDENT*6}"{line}\\n"' for line in brief_version.split('\n') ]) version_stringified = '\n'.join([ f'{INDENT*6}"{line}\\n"' for line in version.split('\n') ]) full_version_stringified = '\n'.join([ f'{INDENT*6}"{line}\\n"' for line in full_version.split('\n') ]) outfile.write(f'''\ /* This file is auto-generated from {meta['infile']}; do not edit */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "{meta['header']}" #include #include #include #include #include #include #ifndef _WIN32 #include #endif /* !_WIN32 */ #include "xsize.h" struct {struct_name} {global_name}; ''') if has_list_arg: outfile.write(f'''\ static void append_to_list (struct {mangle(prog_name)}_list *list, const char *name, const char *arg) {{ {INDENT}const char **tmp; {INDENT}size_t new_count = xsum (list->count, 1); {INDENT}if (size_overflow_p (new_count)) {INDENT*2}error (EXIT_FAILURE, 0, "too many arguments for %s", {INDENT*2} name); {INDENT}tmp = reallocarray (list->args, new_count, sizeof (char *)); {INDENT}if (!tmp) {INDENT*2}error (EXIT_FAILURE, 0, "unable to allocate memory for %s", {INDENT*2} name); {INDENT}list->args = tmp; {INDENT}list->args[list->count] = optarg; {INDENT}list->count = new_count; }} ''') if has_number_arg: outfile.write(f'''\ static long parse_number (const char *arg) {{ {INDENT}char *endptr = NULL; {INDENT}errno = 0; {INDENT}long result; {INDENT}if (strncmp (arg, "0x", 2) == 0) {INDENT*2}result = strtol (arg + 2, &endptr, 16); {INDENT}else if (strncmp (arg, "0", 1) == 0 {INDENT} && strspn (arg, "012345678") == strlen (optarg)) {INDENT*2}result = strtol (arg + 1, &endptr, 8); {INDENT}else {INDENT*2}result = strtol (arg, &endptr, 10); {INDENT}if (errno != 0 || (endptr && *endptr != '\\0')) {INDENT*2}error (EXIT_FAILURE, errno, "'%s' is not a recognizable number.", {INDENT*2} arg); {INDENT}return result; }} ''') outfile.write(f'''\ /* Long options. */ static const struct option long_options[] = {{ {long_opts.getvalue()} }}; int optionProcess (struct {struct_name} *opts, int argc, char **argv) {{ {INDENT}int opt; {enable_statements.getvalue().rstrip()} {INDENT}while ((opt = getopt_long (argc, argv, "{short_opts_concatenated}", {INDENT} long_options, NULL)) != EOF) {INDENT*2}switch (opt) {INDENT*3}{{ {switch_cases.getvalue().rstrip()} {INDENT*3}}} {constraint_statements.getvalue().rstrip()} {argument_statement} {INDENT}if (HAVE_OPT(HELP)) {INDENT*2}{{ {INDENT*3}USAGE(0); {INDENT*2}}} {INDENT}if (HAVE_OPT(MORE_HELP)) #ifdef _WIN32 {INDENT*2}{{ {INDENT*3}USAGE(0); {INDENT*2}}} #else /* _WIN32 */ {INDENT*2}{{ {INDENT*3}pid_t pid; {INDENT*3}int pfds[2]; {INDENT*3}if (pipe (pfds) < 0) {INDENT*4}error (EXIT_FAILURE, errno, "pipe"); {INDENT*3}pid = fork (); {INDENT*3}if (pid < 0) {INDENT*4}error (EXIT_FAILURE, errno, "fork"); {INDENT*3}if (pid == 0) {INDENT*4}{{ {INDENT*5}close (pfds[0]); {INDENT*5}dup2 (pfds[1], STDOUT_FILENO); {INDENT*5}close (pfds[1]); {INDENT*5}usage (stdout, 0); {INDENT*4}}} {INDENT*3}else {INDENT*4}{{ {INDENT*5}const char *args[2]; {INDENT*5}const char *envvar; {INDENT*5}close (pfds[1]); {INDENT*5}dup2 (pfds[0], STDIN_FILENO); {INDENT*5}close (pfds[0]); {INDENT*5}envvar = secure_getenv ("PAGER"); {INDENT*5}if (!envvar || *envvar == '\\0') {INDENT*6}args[0] = "more"; {INDENT*5}else {INDENT*6}args[0] = envvar; {INDENT*5}args[1] = NULL; {INDENT*5}execvp (args[0], (char * const *)args); {INDENT*5}exit (EXIT_FAILURE); {INDENT*4}}} {INDENT*2}}} #endif /* !_WIN32 */ {INDENT}if (HAVE_OPT(VERSION)) {INDENT*2}{{ {INDENT*3}if (!OPT_ARG_VERSION || !strcmp (OPT_ARG_VERSION, "c")) {INDENT*4}{{ {INDENT*5}const char str[] = {version_stringified}; {INDENT*5}fprintf (stdout, "%s", str); {INDENT*5}exit(0); {INDENT*4}}} {INDENT*3}else if (!strcmp (OPT_ARG_VERSION, "v")) {INDENT*4}{{ {INDENT*5}const char str[] = {brief_version_stringified}; {INDENT*5}fprintf (stdout, "%s", str); {INDENT*5}exit(0); {INDENT*4}}} {INDENT*3}else if (!strcmp (OPT_ARG_VERSION, "n")) {INDENT*4}{{ {INDENT*5}const char str[] = {full_version_stringified}; {INDENT*5}fprintf (stdout, "%s", str); {INDENT*5}exit(0); {INDENT*4}}} {INDENT*3}else {INDENT*4}{{ {INDENT*5}error (EXIT_FAILURE, 0, {INDENT*5} "version option argument 'a' invalid. Use:\\n" {INDENT*5} " 'v' - version only\\n" {INDENT*5} " 'c' - version and copyright\\n" {INDENT*5} " 'n' - version and full copyright notice"); {INDENT*4}}} {INDENT*2}}} {INDENT}return optind; }} void usage (FILE *out, int status) {{ {INDENT}const char str[] = {usage_stringified}; {INDENT}fprintf (out, "%s", str); {INDENT}exit (status); }} ''') def gen_h(meta: Mapping[str, str], options: Sequence[Mapping[str, str]], aliases: Mapping[str, Sequence[str]], outfile: TextIO): struct_members_present = io.StringIO() struct_members_arg = io.StringIO() struct_members_value = io.StringIO() struct_members_enabled = io.StringIO() struct_members_list = io.StringIO() have_opts = io.StringIO() opt_args = io.StringIO() opt_values = io.StringIO() enabled_opts = io.StringIO() stackct_opts = io.StringIO() stacklst_opts = io.StringIO() prog_name = meta['prog-name'] struct_name = f'{mangle(prog_name)}_opts' global_name = f'{mangle(prog_name)}Options' list_struct_name = f'{mangle(prog_name)}_list' for option in options: long_opt = option['long-option'] arg_type = option.get('arg-type') lower_opt = mangle(long_opt) upper_opt = lower_opt.upper() # aliases are handled differently if 'aliases' in option: continue struct_members_present.write(f'{INDENT*2}bool {lower_opt};\n') if arg_type: if 'stack-arg' in option: struct_members_list.write( f'{INDENT*2}struct {list_struct_name} {lower_opt};\n' ) stackct_opts.write(( f'#define STACKCT_OPT_{upper_opt} ' f'{global_name}.list.{lower_opt}.count\n' )) stacklst_opts.write(( f'#define STACKLST_OPT_{upper_opt} ' f'{global_name}.list.{lower_opt}.args\n' )) else: struct_members_arg.write( f'{INDENT*2}const char *{lower_opt};\n' ) if arg_type == 'number': struct_members_value.write(f'{INDENT*2}int {lower_opt};\n') opt_values.write(( f'#define OPT_VALUE_{upper_opt} ' f'{global_name}.value.{lower_opt}\n' )) if 'enabled' in option or 'disabled' in option: struct_members_enabled.write(f'{INDENT*2}bool {lower_opt};\n') enabled_opts.write(( f'#define ENABLED_OPT_{upper_opt} ' f'{global_name}.enabled.{lower_opt}\n' )) have_opts.write(( f'#define HAVE_OPT_{upper_opt} ' f'{global_name}.present.{lower_opt}\n' )) opt_args.write(( f'#define OPT_ARG_{upper_opt} ' f'{global_name}.arg.{lower_opt}\n' )) header_guard = f'{mangle(meta["header"]).upper()}_' outfile.write(f'''\ /* This file is auto-generated from {meta["infile"]}; do not edit */ #include #include #ifndef {header_guard} #define {header_guard} 1 struct {list_struct_name} {{ {INDENT}const char **args; {INDENT}unsigned int count; }}; struct {struct_name} {{ {INDENT}/* Options present in the command line */ {INDENT}struct {INDENT}{{ {struct_members_present.getvalue().rstrip()} {INDENT}}} present; {INDENT}/* Option arguments in raw string form */ {INDENT}struct {INDENT}{{ {struct_members_arg.getvalue().rstrip()} {INDENT}}} arg; {INDENT}/* Option arguments parsed as integer */ {INDENT}struct {INDENT}{{ {struct_members_value.getvalue().rstrip()} {INDENT}}} value; {INDENT}/* Option arguments parsed as list */ {INDENT}struct {INDENT}{{ {struct_members_list.getvalue().rstrip()} {INDENT}}} list; {INDENT}/* Option enablement status */ {INDENT}struct {INDENT}{{ {struct_members_enabled.getvalue().rstrip()} {INDENT}}} enabled; }}; #define HAVE_OPT(name) HAVE_OPT_ ## name #define OPT_ARG(name) OPT_ARG_ ## name #define ENABLED_OPT(name) ENABLED_OPT_ ## name #define STACKCT_OPT(name) STACKCT_OPT_ ## name #define STACKLST_OPT(name) STACKLST_OPT_ ## name #define USAGE(status) usage (stdout, (status)) {have_opts.getvalue()} {opt_args.getvalue()} {opt_values.getvalue()} {enabled_opts.getvalue()} {stackct_opts.getvalue()} {stacklst_opts.getvalue()} extern struct {struct_name} {global_name}; int optionProcess(struct {struct_name} *opts, int argc, char **argv); void usage (FILE *out, int status); #endif /* {header_guard} */ ''') def gen(infile: TextIO, meta: Mapping[str, str], c: TextIO, h: TextIO): sections = [jsonopts.Section.from_json(section) for section in json.load(args.json)] sections.append(jsonopts.Section.default()) meta = { **meta, **sections[0].meta, **{ 'header': os.path.basename(h.name), 'infile': os.path.basename(infile.name) } } options = [option for section in sections for option in section.options] aliases = get_aliases(options) usage = jsonopts.usage(meta, sections) gen_c(meta, options, aliases, usage, c) gen_h(meta, options, aliases, h) if __name__ == '__main__': import argparse import json parser = argparse.ArgumentParser(description='generate getopt wrapper') parser.add_argument('json', type=argparse.FileType('r')) parser.add_argument('c', type=argparse.FileType('w')) parser.add_argument('h', type=argparse.FileType('w')) parser.add_argument('--bug-email', help='bug report email address') parser.add_argument('--copyright-year', help='copyright year') parser.add_argument('--copyright-holder', help='copyright holder') parser.add_argument('--license', help='license') parser.add_argument('--version', help='version') args = parser.parse_args() meta = dict() if args.bug_email: meta['bug-email'] = args.bug_email if args.copyright_year: meta['copyright-year'] = args.copyright_year if args.copyright_holder: meta['copyright-holder'] = args.copyright_holder if args.license: meta['license'] = args.license if args.version: meta['version'] = args.version gen(args.json, meta, args.c, args.h)