diff options
| author | Joseph Hunkeler <jhunkeler@users.noreply.github.com> | 2024-07-06 09:49:51 -0400 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-07-06 09:49:51 -0400 | 
| commit | 8ce824ac4b2f526331093a7150e643700efd4d20 (patch) | |
| tree | 99e27dee24e82d78941ded4c510e1bac57c3f015 /src | |
| parent | abe87056faa6ed02aff3bbf77c1fd78b713a0864 (diff) | |
| download | stasis-8ce824ac4b2f526331093a7150e643700efd4d20.tar.gz | |
Add github.c and github.h (#9)
* Add github.c and github.h
* Implements get_github_release_notes()
* Remove unused variables
* Fix circular dependency on tplfunc_frame
* Remove predeclaration of tplfunc_frame
* tpl_register_func accepts pointer to void instead
* tpl_register_func sets maximum number of arguments
* Frame is generated within tpl_register_func
* Improve template function error handling and return/output management
* Remove redundant extern statement
* Include github.h and template_func_proto.h in core.h
* Expose get_github_release_notes_tplfunc_entrypoint function to template engine
* Add template_func_proto.c and template_func_proto.h
* Replace free() with guard variant
* Fix test_template::test_tpl_register_func
* Fix tests
* Fix tests
* cmd should be at least PATH_MAX in size.
* Magic number caused failure to install conda with a long installation path
* Implement get_github_release_notes_auto function that bases release note data off test contexts
* Disable overwriting releases by default
* Add automatic release note generation function call to release_notes.md.in
* Fix test_tpl_register_func()
* Add enough space for tar command plus a path
* Fix circular include
* Github functions do not require access to core.h anyway
* Add comments to union
* Update README to mention template function availability
* Add EnvCtl structure
* Add runtime checks to avoid running all the way to the end only to be met with a configuration error.
* Rename GITHUB to GH
* Development docs pre-rough-draft
Diffstat (limited to 'src')
| -rw-r--r-- | src/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | src/conda.c | 2 | ||||
| -rw-r--r-- | src/delivery.c | 22 | ||||
| -rw-r--r-- | src/github.c | 133 | ||||
| -rw-r--r-- | src/globals.c | 16 | ||||
| -rw-r--r-- | src/ini.c | 2 | ||||
| -rw-r--r-- | src/junitxml.c | 6 | ||||
| -rw-r--r-- | src/stasis_main.c | 49 | ||||
| -rw-r--r-- | src/template.c | 31 | ||||
| -rw-r--r-- | src/template_func_proto.c | 65 | 
10 files changed, 307 insertions, 21 deletions
| diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c42bb0f..a7b06f7 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,6 +22,8 @@ add_library(stasis_core STATIC          rules.c          docker.c          junitxml.c +        github.c +        template_func_proto.c  )  add_executable(stasis diff --git a/src/conda.c b/src/conda.c index 1e8b03f..976bbbc 100644 --- a/src/conda.c +++ b/src/conda.c @@ -32,7 +32,7 @@ int micromamba(struct MicromambaInfo *info, char *command, ...) {      sprintf(mmbin, "%s/micromamba", info->micromamba_prefix);      if (access(mmbin, F_OK)) { -        char untarcmd[PATH_MAX]; +        char untarcmd[PATH_MAX * 2];          mkdirs(info->micromamba_prefix, 0755);          sprintf(untarcmd, "tar -xvf %s -C %s --strip-components=1 bin/micromamba 1>/dev/null", installer_path, info->micromamba_prefix);          system(untarcmd); diff --git a/src/delivery.c b/src/delivery.c index 278647c..7bedef3 100644 --- a/src/delivery.c +++ b/src/delivery.c @@ -1419,7 +1419,7 @@ void delivery_install_conda(char *install_script, char *conda_install_dir) {              // Proceed with the installation              // -b = batch mode (non-interactive) -            char cmd[255] = {0}; +            char cmd[PATH_MAX] = {0};              snprintf(cmd, sizeof(cmd) - 1, "%s %s -b -p %s",                       find_program("bash"),                       install_script, @@ -1476,7 +1476,7 @@ void delivery_defer_packages(struct Delivery *ctx, int type) {      struct StrList *filtered = NULL;      filtered = strlist_init(); -    for (size_t i = 0, z = 0; i < strlist_count(dataptr); i++) { +    for (size_t i = 0; i < strlist_count(dataptr); i++) {          int ignore_pkg = 0;          name = strlist_item(dataptr, i); @@ -1494,7 +1494,6 @@ void delivery_defer_packages(struct Delivery *ctx, int type) {                  if (strstr(name, ctx->tests[x].name)) {                      version = ctx->tests[x].version;                      ignore_pkg = 1; -                    z++;                      break;                  }              } @@ -2164,4 +2163,21 @@ int delivery_fixup_test_results(struct Delivery *ctx) {      closedir(dp);      return 0; +} + +int delivery_exists(struct Delivery *ctx) { +    // TODO: scan artifactory repo for the same information +    char release_pattern[PATH_MAX] = {0}; +    sprintf(release_pattern, "*%s*", ctx->info.release_name); +    struct StrList *files = listdir(ctx->storage.delivery_dir); +    for (size_t i = 0; i < strlist_count(files); i++) { +        char *filename = strlist_item(files, i); +        int release_exists = fnmatch(release_pattern, filename, FNM_PATHNAME); +        if (!globals.enable_overwrite && !release_exists) { +            guard_strlist_free(&files); +            return 1; +        } +    } +    guard_strlist_free(&files); +    return 0;  }
\ No newline at end of file diff --git a/src/github.c b/src/github.c new file mode 100644 index 0000000..36e2e7c --- /dev/null +++ b/src/github.c @@ -0,0 +1,133 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include "core.h" + +struct GHContent { +    char *data; +    size_t len; +}; + +static size_t writer(void *contents, size_t size, size_t nmemb, void *result) { +    const size_t newlen = size * nmemb; +    struct GHContent *content = (struct GHContent *) result; + +    char *ptr = realloc(content->data, content->len + newlen + 1); +    if (!ptr) { +        perror("realloc failed"); +        return 0; +    } + +    content->data = ptr; +    memcpy(&(content->data[content->len]), contents, newlen); +    content->len += newlen; +    content->data[content->len] = 0; + +    return newlen; +} + +static char *unescape_lf(char *value) { +    char *seq = strstr(value, "\\n"); +    while (seq != NULL) { +        size_t cur_len = strlen(seq); +        memmove(seq, seq + 1, strlen(seq) - 1); +        *seq = '\n'; +        if (strlen(seq) && cur_len) { +            seq[cur_len - 1] = 0; +        } +        seq = strstr(value, "\\n"); +    } +    return value; +} + +int get_github_release_notes(const char *api_token, const char *repo, const char *tag, const char *target_commitish, char **output) { +    const char *field_body = "\"body\":\""; +    const char *field_message = "\"message\":\""; +    const char *endpoint_header_auth_fmt = "Authorization: Bearer %s"; +    const char *endpoint_header_api_version = "X-GitHub-Api-Version: " STASIS_GITHUB_API_VERSION; +    const char *endpoint_post_fields_fmt = "{\"tag_name\":\"%s\", \"target_commitish\":\"%s\"}"; +    const char *endpoint_url_fmt = "https://api.github.com/repos/%s/releases/generate-notes"; +    char endpoint_header_auth[PATH_MAX] = {0}; +    char endpoint_post_fields[PATH_MAX] = {0}; +    char endpoint_url[PATH_MAX] = {0}; +    struct curl_slist *list = NULL; +    struct GHContent content; + +    CURL *curl = curl_easy_init(); +    if (!curl) { +        return -1; +    } + +    // Render the header data +    sprintf(endpoint_header_auth, endpoint_header_auth_fmt, api_token); +    sprintf(endpoint_post_fields, endpoint_post_fields_fmt, tag, target_commitish); +    sprintf(endpoint_url, endpoint_url_fmt, repo); + +    // Begin curl configuration +    curl_easy_setopt(curl, CURLOPT_URL, endpoint_url); +    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); +    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, endpoint_post_fields); +    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writer); +    curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *) &content); + +    // Append headers to the request +    list = curl_slist_append(list, "Accept: application/vnd.github+json"); +    list = curl_slist_append(list, endpoint_header_auth); +    list = curl_slist_append(list, endpoint_header_api_version); +    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list); + +    // Set the user-agent (github requires one) +    char user_agent[20] = {0}; +    sprintf(user_agent, "stasis/%s", VERSION); +    curl_easy_setopt(curl, CURLOPT_USERAGENT, user_agent); + +    // Execute curl request +    memset(&content, 0, sizeof(content)); +    CURLcode res; +    res = curl_easy_perform(curl); + +    // Clean up +    curl_slist_free_all(list); +    curl_easy_cleanup(curl); + +    if(res != CURLE_OK) { +        fprintf(stderr, "curl_easy_perform() failed: %s\n", +                curl_easy_strerror(res)); +        return -1; +    } + +    // Replace all "\\n" literals with new line characters +    char *line = unescape_lf(content.data); +    if (line) { +        char *data_offset = NULL; +        if ((data_offset = strstr(line, field_body))) { +            // Skip past the body field +            data_offset += strlen(field_body); +            // Remove quotation mark (and trailing comma if it exists) +            int trim = 2; +            char last_char = data_offset[strlen(data_offset) - trim]; +            if (last_char == ',') { +                trim++; +            } +            data_offset[strlen(data_offset) - trim] = 0; +            // Extract release notes +            *output = strdup(data_offset); +        } else if ((data_offset = strstr(line, field_message))) { +            // Skip past the message field +            data_offset += strlen(field_message); +            *(strchr(data_offset, '"')) = 0; +            fprintf(stderr, "GitHub API Error: '%s'\n", data_offset); +            fprintf(stderr, "URL: %s\n", endpoint_url); +            fprintf(stderr, "POST: %s\n", endpoint_post_fields); +            guard_free(content.data); +            return -1; +        } +    } else { +        fprintf(stderr, "Unknown error\n"); +        guard_free(content.data); +        return -1; +    } + +    guard_free(content.data); +    return 0; +}
\ No newline at end of file diff --git a/src/globals.c b/src/globals.c index 297598f..18a32b5 100644 --- a/src/globals.c +++ b/src/globals.c @@ -36,6 +36,22 @@ struct STASIS_GLOBAL globals = {          .enable_docker = true,          .enable_artifactory = true,          .enable_testing = true, +        .envctl = { +            {.flags = STASIS_ENVCTL_PASSTHRU, .name = {"TMPDIR", NULL}}, +            {.flags = STASIS_ENVCTL_PASSTHRU, .name = {"STASIS_ROOT", NULL}}, +            {.flags = STASIS_ENVCTL_PASSTHRU, .name = {"STASIS_SYSCONFDIR", NULL}}, +            {.flags = STASIS_ENVCTL_PASSTHRU, .name = {"STASIS_CPU_COUNT", "CPU_COUNT", NULL}}, +            {.flags = STASIS_ENVCTL_REQUIRED | STASIS_ENVCTL_REDACT, .name={"STASIS_GH_TOKEN", "GITHUB_TOKEN", NULL}}, +            {.flags = STASIS_ENVCTL_REDACT, .name = {"STASIS_JF_ACCESS_TOKEN", NULL}}, +            {.flags = STASIS_ENVCTL_PASSTHRU, .name = {"STASIS_JF_USER", NULL}}, +            {.flags = STASIS_ENVCTL_REDACT, .name = {"STASIS_JF_PASSWORD", NULL}}, +            {.flags = STASIS_ENVCTL_REDACT, .name = {"STASIS_JF_SSH_KEY_PATH", NULL}}, +            {.flags = STASIS_ENVCTL_REDACT, .name = {"STASIS_JF_SSH_PASSPHRASE", NULL}}, +            {.flags = STASIS_ENVCTL_REDACT, .name = {"STASIS_JF_CLIENT_CERT_CERT_PATH", NULL}}, +            {.flags = STASIS_ENVCTL_REDACT, .name = {"STASIS_JF_CLIENT_CERT_KEY_PATH", NULL}}, +            {.flags = STASIS_ENVCTL_REQUIRED, .name = {"STASIS_JF_REPO", NULL}}, +            {.flags = 0, .name = {NULL}}, +        }  };  void globals_free() { @@ -153,7 +153,7 @@ int ini_getval(struct INIFILE *ini, char *section_name, char *key, int type, uni          case INIVAL_TYPE_STR_ARRAY:              strcpy(tbufp, data->value);              *data->value = '\0'; -            for (size_t i = 0; (token = strsep(&tbufp, "\n")) != NULL; i++) { +            while ((token = strsep(&tbufp, "\n")) != NULL) {                  lstrip(token);                  strcat(data->value, token);                  strcat(data->value, "\n"); diff --git a/src/junitxml.c b/src/junitxml.c index c1bf1be..9c7e5b4 100644 --- a/src/junitxml.c +++ b/src/junitxml.c @@ -146,16 +146,16 @@ static struct StrList *attributes_to_strlist(xmlTextReaderPtr reader) {  static int read_xml_data(xmlTextReaderPtr reader, struct JUNIT_Testsuite **testsuite) {      const xmlChar *name; -    const xmlChar *value; +    //const xmlChar *value;      name = xmlTextReaderConstName(reader);      if (!name) {          // name could not be converted to string          name = BAD_CAST "--";      } -    value = xmlTextReaderConstValue(reader); +    //value = xmlTextReaderConstValue(reader);      const char *node_name = (char *) name; -    const char *node_value = (char *) value; +    //const char *node_value = (char *) value;      struct StrList *attrs = attributes_to_strlist(reader);      if (attrs && strlist_count(attrs)) { diff --git a/src/stasis_main.c b/src/stasis_main.c index c550982..ce49829 100644 --- a/src/stasis_main.c +++ b/src/stasis_main.c @@ -9,6 +9,7 @@  #define OPT_NO_DOCKER 1001  #define OPT_NO_ARTIFACTORY 1002  #define OPT_NO_TESTING 1003 +#define OPT_OVERWRITE 1004  static struct option long_options[] = {          {"help", no_argument, 0, 'h'},          {"version", no_argument, 0, 'V'}, @@ -18,6 +19,7 @@ static struct option long_options[] = {          {"verbose", no_argument, 0, 'v'},          {"unbuffered", no_argument, 0, 'U'},          {"update-base", no_argument, 0, OPT_ALWAYS_UPDATE_BASE}, +        {"overwrite", no_argument, 0, OPT_OVERWRITE},          {"no-docker", no_argument, 0, OPT_NO_DOCKER},          {"no-artifactory", no_argument, 0, OPT_NO_ARTIFACTORY},          {"no-testing", no_argument, 0, OPT_NO_TESTING}, @@ -33,6 +35,7 @@ const char *long_options_help[] = {          "Increase output verbosity",          "Disable line buffering",          "Update conda installation prior to STASIS environment creation", +        "Overwrite an existing release",          "Do not build docker images",          "Do not upload artifacts to Artifactory",          "Do not execute test scripts", @@ -95,7 +98,32 @@ static void usage(char *progname) {      }  } +static const char *has_envctl_key_(size_t i) { +    for (size_t x = 0; globals.envctl[i].name[x] != NULL; x++) { +        const char *name = globals.envctl[i].name[x]; +        const char *data = getenv(name); +        if (data) { +            return name; +        } +    } +    return NULL; +} + +static void check_system_env_requirements() { +    msg(STASIS_MSG_L1, "Checking environment\n"); +    for (size_t i = 0; globals.envctl[i].name[0] != NULL; i++) { +        unsigned int flags = globals.envctl[i].flags; +        const char *key = has_envctl_key_(i); +        if ((flags & STASIS_ENVCTL_REQUIRED) && !(key && strlen(getenv(key)))) { +            if (!strcmp(key, "STASIS_JF_REPO") && !globals.enable_artifactory) { +                continue; +            } +            msg(STASIS_MSG_L2 | STASIS_MSG_ERROR, "Environment variable '%s' must be configured.\n", globals.envctl[i].name[0]); +            exit(1); +        } +    } +}  static void check_system_requirements(struct Delivery *ctx) {      const char *tools_required[] = { @@ -139,6 +167,11 @@ static void check_system_requirements(struct Delivery *ctx) {      }  } +static void check_requirements(struct Delivery *ctx) { +    check_system_requirements(ctx); +    check_system_env_requirements(); +} +  int main(int argc, char *argv[]) {      struct Delivery ctx;      struct Process proc = { @@ -193,6 +226,9 @@ int main(int argc, char *argv[]) {              case 'v':                  globals.verbose = true;                  break; +            case OPT_OVERWRITE: +                globals.enable_overwrite = true; +                break;              case OPT_NO_DOCKER:                  globals.enable_docker = false;                  user_disabled_docker = true; @@ -260,6 +296,11 @@ int main(int argc, char *argv[]) {      tpl_register("workaround.tox_posargs", &globals.workaround.tox_posargs);      tpl_register("workaround.conda_reactivate", &globals.workaround.conda_reactivate); +    // Expose function(s) to the template engine +    // Prototypes can be found in template_func_proto.h +    tpl_register_func("get_github_release_notes", &get_github_release_notes_tplfunc_entrypoint, 3, NULL); +    tpl_register_func("get_github_release_notes_auto", &get_github_release_notes_auto_tplfunc_entrypoint, 1, &ctx); +      // Set up PREFIX/etc directory information      // The user may manipulate the base directory path with STASIS_SYSCONFDIR      // environment variable @@ -325,7 +366,7 @@ int main(int argc, char *argv[]) {          msg(STASIS_MSG_ERROR | STASIS_MSG_L2, "Failed to initialize delivery context\n");          exit(1);      } -    check_system_requirements(&ctx); +    check_requirements(&ctx);      msg(STASIS_MSG_L2, "Configuring JFrog CLI\n");      if (delivery_init_artifactory(&ctx)) { @@ -346,6 +387,12 @@ int main(int argc, char *argv[]) {          //delivery_runtime_show(&ctx);      } +    // Safety gate: Avoid clobbering a delivery unless the user wants that behavior +    if (delivery_exists(&ctx)) { +        msg(STASIS_MSG_ERROR | STASIS_MSG_L1, "Refusing to overwrite delivery: %s\nUse --overwrite to enable release clobbering.\n", ctx.info.release_name); +        exit(1); +    } +      msg(STASIS_MSG_L1, "Conda setup\n");      delivery_get_installer_url(&ctx, installer_url);      msg(STASIS_MSG_L2, "Downloading: %s\n", installer_url); diff --git a/src/template.c b/src/template.c index 819fe92..8e1357c 100644 --- a/src/template.c +++ b/src/template.c @@ -25,10 +25,14 @@ extern void tpl_reset() {      tpl_pool_func_used = 0;  } -void tpl_register_func(char *key, struct tplfunc_frame *frame) { -    (void) key;  // TODO: placeholder -    tpl_pool_func[tpl_pool_func_used] = calloc(1, sizeof(*tpl_pool_func[tpl_pool_func_used])); -    memcpy(tpl_pool_func[tpl_pool_func_used], frame, sizeof(*frame)); +void tpl_register_func(char *key, void *tplfunc_ptr, int argc, void *data_in) { +    struct tplfunc_frame *frame = calloc(1, sizeof(*frame)); +    frame->key = strdup(key); +    frame->argc = argc; +    frame->func = tplfunc_ptr; +    frame->data_in = data_in; + +    tpl_pool_func[tpl_pool_func_used] = frame;      tpl_pool_func_used++;  } @@ -220,7 +224,7 @@ char *tpl_render(char *str) {                  strcpy(func_name_temp, type_stop + 1);                  char *param_begin = strchr(func_name_temp, '(');                  if (!param_begin) { -                    fprintf(stderr, "offset %zu: function name must be followed by a '('\n", off); +                    fprintf(stderr, "At position %zu in %s\nfunction name must be followed by a '('\n", off, key);                      guard_free(output);                      return NULL;                  } @@ -228,7 +232,7 @@ char *tpl_render(char *str) {                  param_begin++;                  char *param_end = strrchr(param_begin, ')');                  if (!param_end) { -                    fprintf(stderr, "offset %zu: function arguments must be closed with a ')'\n", off); +                    fprintf(stderr, "At position %zu in %s\nfunction arguments must be closed with a ')'\n", off, key);                      guard_free(output);                      return NULL;                  } @@ -239,8 +243,8 @@ char *tpl_render(char *str) {                  for (params_count = 0; params[params_count] != NULL; params_count++);                  struct tplfunc_frame *frame = tpl_getfunc(k); -                if (params_count > frame->argc) { -                    fprintf(stderr, "offset %zu: Too many arguments for function: %s()\n", off, frame->key); +                if (params_count > frame->argc || params_count < frame->argc) { +                    fprintf(stderr, "At position %zu in %s\nIncorrect number of arguments for function: %s (expected %d, got %d)\n", off, key, frame->key, frame->argc, params_count);                      value = strdup("");                  } else {                      for (size_t p = 0; p < sizeof(frame->argv) / sizeof(*frame->argv) && params[p] != NULL; p++) { @@ -248,10 +252,13 @@ char *tpl_render(char *str) {                          strip(params[p]);                          frame->argv[p].t_char_ptr = params[p];                      } -                    char func_result[100]; -                    char *fres = func_result; -                    frame->func(frame, fres); -                    value = strdup(fres); +                    char *func_result = NULL; +                    int func_status = 0; +                    if ((func_status = frame->func(frame, &func_result))) { +                        fprintf(stderr, "%s returned non-zero status: %d\n", frame->key, func_status); +                    } +                    value = strdup(func_result ? func_result : ""); +                    guard_free(func_result);                  }                  GENERIC_ARRAY_FREE(params);              } else { diff --git a/src/template_func_proto.c b/src/template_func_proto.c new file mode 100644 index 0000000..140a5e0 --- /dev/null +++ b/src/template_func_proto.c @@ -0,0 +1,65 @@ +#include "template_func_proto.h" + +int get_github_release_notes_tplfunc_entrypoint(void *frame, void *data_out) { +    int result; +    char **output = (char **) data_out; +    struct tplfunc_frame *f = (struct tplfunc_frame *) frame; +    char *api_token = getenv("STASIS_GH_TOKEN"); +    if (!api_token) { +        api_token = getenv("GITHUB_TOKEN"); +    } +    result = get_github_release_notes(api_token ? api_token : "anonymous", +                                      (const char *) f->argv[0].t_char_ptr, +                                      (const char *) f->argv[1].t_char_ptr, +                                      (const char *) f->argv[2].t_char_ptr, +                                      output); +    return result; +} + +int get_github_release_notes_auto_tplfunc_entrypoint(void *frame, void *data_out) { +    int result = 0; +    char **output = (char **) data_out; +    struct tplfunc_frame *f = (struct tplfunc_frame *) frame; +    char *api_token = getenv("STASIS_GITHUB_TOKEN"); +    if (!api_token) { +        api_token = getenv("GITHUB_TOKEN"); +    } + +    const struct Delivery *ctx = (struct Delivery *) f->data_in; +    struct StrList *notes_list = strlist_init(); +    for (size_t i = 0; i < sizeof(ctx->tests) / sizeof(*ctx->tests); i++) { +        // Get test context +        const struct Test *test = &ctx->tests[i]; +        if (test->name && test->version && test->repository) { +            char *repository = strdup(test->repository); +            char *match = strstr(repository, "spacetelescope/"); +            // Cull repository URL +            if (match) { +                replace_text(repository, "https://github.com/", "", 0); +                if (endswith(repository, ".git")) { +                    replace_text(repository, ".git", "", 0); +                } +                // Record release notes for version relative to HEAD +                // Using HEAD, GitHub returns the previous tag +                char *note = NULL; +                char h1_title[NAME_MAX] = {0}; +                sprintf(h1_title, "# %s", test->name); +                strlist_append(¬es_list, h1_title); +                result += get_github_release_notes(api_token ? api_token : "anonymous", +                                                   repository, +                                                   test->version, +                                                   "HEAD", +                                                   ¬e); +                if (note) { +                    strlist_append(¬es_list, note); +                    guard_free(note); +                } +            } +        } +    } +    // Return all notes as a single string +    if (strlist_count(notes_list)) { +        *output = join(notes_list->data, "\n\n"); +    } +    return result; +} | 
