From cf6ba06cd445f1f4554701637d5bab581acfba98 Mon Sep 17 00:00:00 2001 From: dartraiden Date: Mon, 21 Apr 2025 09:33:29 +0300 Subject: libcurl: update to 8.13.0 --- libs/libcurl/src/vssh/libssh2.c | 311 ++++++++++++++++++++++++---------------- 1 file changed, 185 insertions(+), 126 deletions(-) (limited to 'libs/libcurl/src/vssh/libssh2.c') diff --git a/libs/libcurl/src/vssh/libssh2.c b/libs/libcurl/src/vssh/libssh2.c index a75397e95c..4ecf198e17 100644 --- a/libs/libcurl/src/vssh/libssh2.c +++ b/libs/libcurl/src/vssh/libssh2.c @@ -68,12 +68,11 @@ #include "inet_ntop.h" #include "parsedate.h" /* for the week day and month names */ #include "sockaddr.h" /* required for Curl_sockaddr_storage */ -#include "strtoofft.h" #include "multiif.h" #include "select.h" #include "warnless.h" #include "curl_path.h" - +#include "strparse.h" #include /* for base64 encoding/decoding */ #include @@ -107,7 +106,8 @@ static int ssh_getsock(struct Curl_easy *data, struct connectdata *conn, static CURLcode ssh_setup_connection(struct Curl_easy *data, struct connectdata *conn); static void ssh_attach(struct Curl_easy *data, struct connectdata *conn); - +static int sshc_cleanup(struct ssh_conn *sshc, struct Curl_easy *data, + bool block); /* * SCP protocol handler. */ @@ -369,7 +369,7 @@ static void state(struct Curl_easy *data, sshstate nowstate) }; /* a precaution to make sure the lists are in sync */ - DEBUGASSERT(sizeof(names)/sizeof(names[0]) == SSH_LAST); + DEBUGASSERT(CURL_ARRAYSIZE(names) == SSH_LAST); if(sshc->state != nowstate) { infof(data, "SFTP %p state change from %s to %s", @@ -598,9 +598,9 @@ static CURLcode ssh_check_fingerprint(struct Curl_easy *data) const char *pubkey_sha256 = data->set.str[STRING_SSH_HOST_PUBLIC_KEY_SHA256]; infof(data, "SSH MD5 public key: %s", - pubkey_md5 != NULL ? pubkey_md5 : "NULL"); + pubkey_md5 != NULL ? pubkey_md5 : "NULL"); infof(data, "SSH SHA256 public key: %s", - pubkey_sha256 != NULL ? pubkey_sha256 : "NULL"); + pubkey_sha256 != NULL ? pubkey_sha256 : "NULL"); if(pubkey_sha256) { const char *fingerprint = NULL; @@ -684,7 +684,7 @@ static CURLcode ssh_check_fingerprint(struct Curl_easy *data) if(pubkey_md5) { char md5buffer[33]; - const char *fingerprint = NULL; + const char *fingerprint; fingerprint = libssh2_hostkey_hash(sshc->ssh_session, LIBSSH2_HOSTKEY_HASH_MD5); @@ -796,7 +796,9 @@ static CURLcode ssh_force_knownhost_key_type(struct Curl_easy *data) int port = 0; bool found = FALSE; - if(sshc->kh && !data->set.str[STRING_SSH_HOST_PUBLIC_KEY_MD5]) { + if(sshc->kh && + !data->set.str[STRING_SSH_HOST_PUBLIC_KEY_MD5] && + !data->set.str[STRING_SSH_HOST_PUBLIC_KEY_SHA256]) { /* lets try to find our host in the known hosts file */ while(!libssh2_knownhost_get(sshc->kh, &store, store)) { /* For non-standard ports, the name will be enclosed in */ @@ -931,7 +933,7 @@ static CURLcode sftp_quote(struct Curl_easy *data, char *tmp = aprintf("257 \"%s\" is current directory.\n", sshp->path); if(!tmp) return CURLE_OUT_OF_MEMORY; - Curl_debug(data, CURLINFO_HEADER_OUT, (char *)"PWD\n", 4); + Curl_debug(data, CURLINFO_HEADER_OUT, "PWD\n", 4); Curl_debug(data, CURLINFO_HEADER_IN, tmp, strlen(tmp)); /* this sends an FTP-like "header" to the header callback so that the @@ -970,11 +972,11 @@ static CURLcode sftp_quote(struct Curl_easy *data, * Instead, we scan for commands used by OpenSSH's sftp program and call the * appropriate libssh2 functions. */ - if(strncasecompare(cmd, "chgrp ", 6) || - strncasecompare(cmd, "chmod ", 6) || - strncasecompare(cmd, "chown ", 6) || - strncasecompare(cmd, "atime ", 6) || - strncasecompare(cmd, "mtime ", 6)) { + if(!strncmp(cmd, "chgrp ", 6) || + !strncmp(cmd, "chmod ", 6) || + !strncmp(cmd, "chown ", 6) || + !strncmp(cmd, "atime ", 6) || + !strncmp(cmd, "mtime ", 6)) { /* attribute change */ /* sshc->quote_path1 contains the mode to set */ @@ -990,8 +992,8 @@ static CURLcode sftp_quote(struct Curl_easy *data, state(data, SSH_SFTP_QUOTE_STAT); return result; } - if(strncasecompare(cmd, "ln ", 3) || - strncasecompare(cmd, "symlink ", 8)) { + if(!strncmp(cmd, "ln ", 3) || + !strncmp(cmd, "symlink ", 8)) { /* symbolic linking */ /* sshc->quote_path1 is the source */ /* get the destination */ @@ -1005,12 +1007,12 @@ static CURLcode sftp_quote(struct Curl_easy *data, state(data, SSH_SFTP_QUOTE_SYMLINK); return result; } - else if(strncasecompare(cmd, "mkdir ", 6)) { + else if(!strncmp(cmd, "mkdir ", 6)) { /* create dir */ state(data, SSH_SFTP_QUOTE_MKDIR); return result; } - else if(strncasecompare(cmd, "rename ", 7)) { + else if(!strncmp(cmd, "rename ", 7)) { /* rename file */ /* first param is the source path */ /* second param is the dest. path */ @@ -1024,16 +1026,16 @@ static CURLcode sftp_quote(struct Curl_easy *data, state(data, SSH_SFTP_QUOTE_RENAME); return result; } - else if(strncasecompare(cmd, "rmdir ", 6)) { + else if(!strncmp(cmd, "rmdir ", 6)) { /* delete dir */ state(data, SSH_SFTP_QUOTE_RMDIR); return result; } - else if(strncasecompare(cmd, "rm ", 3)) { + else if(!strncmp(cmd, "rm ", 3)) { state(data, SSH_SFTP_QUOTE_UNLINK); return result; } - else if(strncasecompare(cmd, "statvfs ", 8)) { + else if(!strncmp(cmd, "statvfs ", 8)) { state(data, SSH_SFTP_QUOTE_STATVFS); return result; } @@ -1263,7 +1265,7 @@ sftp_pkey_init(struct Curl_easy *data, if(!sshc->rsa) out_of_memory = TRUE; else if(stat(sshc->rsa, &sbuf)) { - Curl_safefree(sshc->rsa); + free(sshc->rsa); sshc->rsa = aprintf("%s/.ssh/id_dsa", home); if(!sshc->rsa) out_of_memory = TRUE; @@ -1277,10 +1279,10 @@ sftp_pkey_init(struct Curl_easy *data, /* Nothing found; try the current dir. */ sshc->rsa = strdup("id_rsa"); if(sshc->rsa && stat(sshc->rsa, &sbuf)) { - Curl_safefree(sshc->rsa); + free(sshc->rsa); sshc->rsa = strdup("id_dsa"); if(sshc->rsa && stat(sshc->rsa, &sbuf)) { - Curl_safefree(sshc->rsa); + free(sshc->rsa); /* Out of guesses. Set to the empty string to avoid * surprising info messages. */ sshc->rsa = strdup(""); @@ -1345,7 +1347,7 @@ sftp_quote_stat(struct Curl_easy *data, sshc->acceptfail = TRUE; } - if(!strncasecompare(cmd, "chmod", 5)) { + if(!!strncmp(cmd, "chmod", 5)) { /* Since chown and chgrp only set owner OR group but libssh2 wants to set * them both at once, we need to obtain the current ownership first. This * takes an extra protocol round trip. @@ -1367,8 +1369,11 @@ sftp_quote_stat(struct Curl_easy *data, } /* Now set the new attributes... */ - if(strncasecompare(cmd, "chgrp", 5)) { - sshp->quote_attrs.gid = strtoul(sshc->quote_path1, NULL, 10); + if(!strncmp(cmd, "chgrp", 5)) { + const char *p = sshc->quote_path1; + curl_off_t gid; + (void)Curl_str_number(&p, &gid, ULONG_MAX); + sshp->quote_attrs.gid = (unsigned long)gid; sshp->quote_attrs.flags = LIBSSH2_SFTP_ATTR_UIDGID; if(sshp->quote_attrs.gid == 0 && !ISDIGIT(sshc->quote_path1[0]) && !sshc->acceptfail) { @@ -1376,18 +1381,23 @@ sftp_quote_stat(struct Curl_easy *data, goto fail; } } - else if(strncasecompare(cmd, "chmod", 5)) { - sshp->quote_attrs.permissions = strtoul(sshc->quote_path1, NULL, 8); - sshp->quote_attrs.flags = LIBSSH2_SFTP_ATTR_PERMISSIONS; + else if(!strncmp(cmd, "chmod", 5)) { + curl_off_t perms; + const char *p = sshc->quote_path1; /* permissions are octal */ - if(sshp->quote_attrs.permissions == 0 && - !ISDIGIT(sshc->quote_path1[0])) { + if(Curl_str_octal(&p, &perms, 07777)) { failf(data, "Syntax error: chmod permissions not a number"); goto fail; } + + sshp->quote_attrs.permissions = (unsigned long)perms; + sshp->quote_attrs.flags = LIBSSH2_SFTP_ATTR_PERMISSIONS; } - else if(strncasecompare(cmd, "chown", 5)) { - sshp->quote_attrs.uid = strtoul(sshc->quote_path1, NULL, 10); + else if(!strncmp(cmd, "chown", 5)) { + const char *p = sshc->quote_path1; + curl_off_t uid; + (void)Curl_str_number(&p, &uid, ULONG_MAX); + sshp->quote_attrs.uid = (unsigned long)uid; sshp->quote_attrs.flags = LIBSSH2_SFTP_ATTR_UIDGID; if(sshp->quote_attrs.uid == 0 && !ISDIGIT(sshc->quote_path1[0]) && !sshc->acceptfail) { @@ -1395,8 +1405,8 @@ sftp_quote_stat(struct Curl_easy *data, goto fail; } } - else if(strncasecompare(cmd, "atime", 5) || - strncasecompare(cmd, "mtime", 5)) { + else if(!strncmp(cmd, "atime", 5) || + !strncmp(cmd, "mtime", 5)) { time_t date = Curl_getdate_capped(sshc->quote_path1); bool fail = FALSE; @@ -1413,7 +1423,7 @@ sftp_quote_stat(struct Curl_easy *data, #endif if(fail) goto fail; - if(strncasecompare(cmd, "atime", 5)) + if(!strncmp(cmd, "atime", 5)) sshp->quote_attrs.atime = (unsigned long)date; else /* mtime */ sshp->quote_attrs.mtime = (unsigned long)date; @@ -1466,20 +1476,19 @@ sftp_download_stat(struct Curl_easy *data, } if(data->state.use_range) { curl_off_t from, to; - char *ptr; - char *ptr2; - CURLofft to_t; - CURLofft from_t; + const char *p = data->state.range; + int to_t, from_t; - from_t = curlx_strtoofft(data->state.range, &ptr, 10, &from); - if(from_t == CURL_OFFT_FLOW) + from_t = Curl_str_number(&p, &from, CURL_OFF_T_MAX); + if(from_t == STRE_OVERFLOW) return CURLE_RANGE_ERROR; - while(*ptr && (ISBLANK(*ptr) || (*ptr == '-'))) - ptr++; - to_t = curlx_strtoofft(ptr, &ptr2, 10, &to); - if(to_t == CURL_OFFT_FLOW) + Curl_str_passblanks(&p); + (void)Curl_str_single(&p, '-'); + + to_t = Curl_str_numblanks(&p, &to); + if(to_t == STRE_OVERFLOW) return CURLE_RANGE_ERROR; - if((to_t == CURL_OFFT_INVAL) /* no "to" value given */ + if((to_t == STRE_NO_NUM) /* no "to" value given */ || (to >= size)) { to = size - 1; } @@ -1585,8 +1594,7 @@ static CURLcode sftp_readdir(struct Curl_easy *data, sshp->readdir_filename, readdir_len); if(!result) - result = Curl_client_write(data, CLIENTWRITE_BODY, - (char *)"\n", 1); + result = Curl_client_write(data, CLIENTWRITE_BODY, "\n", 1); if(result) return result; } @@ -1597,7 +1605,6 @@ static CURLcode sftp_readdir(struct Curl_easy *data, if((sshp->readdir_attrs.flags & LIBSSH2_SFTP_ATTR_PERMISSIONS) && ((sshp->readdir_attrs.permissions & LIBSSH2_SFTP_S_IFMT) == LIBSSH2_SFTP_S_IFLNK)) { - Curl_dyn_init(&sshp->readdir_link, CURL_PATH_MAX); result = Curl_dyn_addf(&sshp->readdir_link, "%s%s", sshp->path, sshp->readdir_filename); state(data, SSH_SFTP_READDIR_LINK); @@ -1995,13 +2002,17 @@ static CURLcode ssh_statemachine(struct Curl_easy *data, bool *block) if(rc > 0) { /* It seems that this string is not always NULL terminated */ sshp->readdir_filename[rc] = '\0'; + free(sshc->homedir); sshc->homedir = strdup(sshp->readdir_filename); if(!sshc->homedir) { state(data, SSH_SFTP_CLOSE); sshc->actualcode = CURLE_OUT_OF_MEMORY; break; } - data->state.most_recent_ftp_entrypath = sshc->homedir; + free(data->state.most_recent_ftp_entrypath); + data->state.most_recent_ftp_entrypath = strdup(sshc->homedir); + if(!data->state.most_recent_ftp_entrypath) + return CURLE_OUT_OF_MEMORY; } else { /* Return the error type */ @@ -2019,13 +2030,13 @@ static CURLcode ssh_statemachine(struct Curl_easy *data, bool *block) break; } - /* This is the last step in the SFTP connect phase. Do note that while - we get the homedir here, we get the "workingpath" in the DO action - since the homedir will remain the same between request but the - working path will not. */ - DEBUGF(infof(data, "SSH CONNECT phase done")); - state(data, SSH_STOP); - break; + /* This is the last step in the SFTP connect phase. Do note that while + we get the homedir here, we get the "workingpath" in the DO action + since the homedir will remain the same between request but the + working path will not. */ + DEBUGF(infof(data, "SSH CONNECT phase done")); + state(data, SSH_STOP); + break; case SSH_SFTP_QUOTE_INIT: @@ -2288,7 +2299,6 @@ static CURLcode ssh_statemachine(struct Curl_easy *data, bool *block) } case SSH_SFTP_GETINFO: - { if(data->set.get_filetime) { state(data, SSH_SFTP_FILETIME); } @@ -2296,7 +2306,6 @@ static CURLcode ssh_statemachine(struct Curl_easy *data, bool *block) state(data, SSH_SFTP_TRANS_INIT); } break; - } case SSH_SFTP_FILETIME: { @@ -2418,7 +2427,6 @@ static CURLcode ssh_statemachine(struct Curl_easy *data, bool *block) sshc->actualcode = result ? result : CURLE_SSH; break; } - Curl_dyn_init(&sshp->readdir, CURL_PATH_MAX * 2); state(data, SSH_SFTP_READDIR); break; @@ -2584,7 +2592,6 @@ static CURLcode ssh_statemachine(struct Curl_easy *data, bool *block) } Curl_safefree(sshc->homedir); - data->state.most_recent_ftp_entrypath = NULL; state(data, SSH_SESSION_DISCONNECT); break; @@ -2850,72 +2857,17 @@ static CURLcode ssh_statemachine(struct Curl_easy *data, bool *block) } Curl_safefree(sshc->homedir); - data->state.most_recent_ftp_entrypath = NULL; state(data, SSH_SESSION_FREE); break; case SSH_SESSION_FREE: - if(sshc->kh) { - libssh2_knownhost_free(sshc->kh); - sshc->kh = NULL; - } - - if(sshc->ssh_agent) { - rc = libssh2_agent_disconnect(sshc->ssh_agent); - if(rc == LIBSSH2_ERROR_EAGAIN) { - break; - } - if(rc < 0) { - char *err_msg = NULL; - (void)libssh2_session_last_error(sshc->ssh_session, - &err_msg, NULL, 0); - infof(data, "Failed to disconnect from libssh2 agent: %d %s", - rc, err_msg); - } - libssh2_agent_free(sshc->ssh_agent); - sshc->ssh_agent = NULL; - - /* NB: there is no need to free identities, they are part of internal - agent stuff */ - sshc->sshagent_identity = NULL; - sshc->sshagent_prev_identity = NULL; - } - - if(sshc->ssh_session) { - rc = libssh2_session_free(sshc->ssh_session); - if(rc == LIBSSH2_ERROR_EAGAIN) { - break; - } - if(rc < 0) { - char *err_msg = NULL; - (void)libssh2_session_last_error(sshc->ssh_session, - &err_msg, NULL, 0); - infof(data, "Failed to free libssh2 session: %d %s", rc, err_msg); - } - sshc->ssh_session = NULL; - } - - /* worst-case scenario cleanup */ - - DEBUGASSERT(sshc->ssh_session == NULL); - DEBUGASSERT(sshc->ssh_channel == NULL); - DEBUGASSERT(sshc->sftp_session == NULL); - DEBUGASSERT(sshc->sftp_handle == NULL); - DEBUGASSERT(sshc->kh == NULL); - DEBUGASSERT(sshc->ssh_agent == NULL); - - Curl_safefree(sshc->rsa_pub); - Curl_safefree(sshc->rsa); - Curl_safefree(sshc->quote_path1); - Curl_safefree(sshc->quote_path2); - Curl_safefree(sshc->homedir); - + rc = sshc_cleanup(sshc, data, FALSE); + if(rc == LIBSSH2_ERROR_EAGAIN) + break; /* the code we are about to return */ result = sshc->actualcode; - memset(sshc, 0, sizeof(struct ssh_conn)); - connclose(conn, "SSH session free"); sshc->state = SSH_SESSION_FREE; /* current */ sshc->nextstate = SSH_NO_STATE; @@ -3068,13 +3020,23 @@ static CURLcode ssh_block_statemach(struct Curl_easy *data, static CURLcode ssh_setup_connection(struct Curl_easy *data, struct connectdata *conn) { + struct ssh_conn *sshc = &conn->proto.sshc; struct SSHPROTO *ssh; (void)conn; + if(!sshc->initialised) { + /* other ssh implementations do something here, let's keep + * the initialised flag correct even if this implementation does not. */ + sshc->initialised = TRUE; + } + data->req.p.ssh = ssh = calloc(1, sizeof(struct SSHPROTO)); if(!ssh) return CURLE_OUT_OF_MEMORY; + Curl_dyn_init(&ssh->readdir, CURL_PATH_MAX * 2); + Curl_dyn_init(&ssh->readdir_link, CURL_PATH_MAX); + return CURLE_OK; } @@ -3103,7 +3065,7 @@ static ssize_t ssh_tls_recv(libssh2_socket_t sock, void *buffer, return -EAGAIN; /* magic return code for libssh2 */ else if(result) return -1; /* generic error */ - Curl_debug(data, CURLINFO_DATA_IN, (char *)buffer, (size_t)nread); + Curl_debug(data, CURLINFO_DATA_IN, (const char *)buffer, (size_t)nread); return nread; } @@ -3128,7 +3090,7 @@ static ssize_t ssh_tls_send(libssh2_socket_t sock, const void *buffer, return -EAGAIN; /* magic return code for libssh2 */ else if(result) return -1; /* error */ - Curl_debug(data, CURLINFO_DATA_OUT, (char *)buffer, nwrite); + Curl_debug(data, CURLINFO_DATA_OUT, (const char *)buffer, nwrite); return (ssize_t)nwrite; } #endif @@ -3146,6 +3108,34 @@ static CURLcode ssh_connect(struct Curl_easy *data, bool *done) CURLcode result; struct connectdata *conn = data->conn; +#if LIBSSH2_VERSION_NUM >= 0x010b00 + { + const char *crypto_str; + switch(libssh2_crypto_engine()) { + case libssh2_gcrypt: + crypto_str = "libgcrypt"; + break; + case libssh2_mbedtls: + crypto_str = "mbedTLS"; + break; + case libssh2_openssl: + crypto_str = "openssl compatible"; + break; + case libssh2_os400qc3: + crypto_str = "OS400QC3"; + break; + case libssh2_wincng: + crypto_str = "WinCNG"; + break; + default: + crypto_str = NULL; + break; + } + if(crypto_str) + infof(data, "libssh2 cryptography backend: %s", crypto_str); + } +#endif + /* initialize per-handle data if not already */ if(!data->req.p.ssh) { result = ssh_setup_connection(data, conn); @@ -3159,10 +3149,11 @@ static CURLcode ssh_connect(struct Curl_easy *data, bool *done) sshc = &conn->proto.sshc; + if(conn->user) + infof(data, "User: '%s'", conn->user); + else + infof(data, "User: NULL"); #ifdef CURL_LIBSSH2_DEBUG - if(conn->user) { - infof(data, "User: %s", conn->user); - } if(conn->passwd) { infof(data, "Password: %s", conn->passwd); } @@ -3369,6 +3360,71 @@ static CURLcode ssh_do(struct Curl_easy *data, bool *done) return result; } +static int sshc_cleanup(struct ssh_conn *sshc, struct Curl_easy *data, + bool block) +{ + int rc; + + if(sshc->initialised) { + if(sshc->kh) { + libssh2_knownhost_free(sshc->kh); + sshc->kh = NULL; + } + + if(sshc->ssh_agent) { + rc = libssh2_agent_disconnect(sshc->ssh_agent); + if(!block && (rc == LIBSSH2_ERROR_EAGAIN)) { + return rc; + } + if(rc < 0) { + char *err_msg = NULL; + (void)libssh2_session_last_error(sshc->ssh_session, + &err_msg, NULL, 0); + infof(data, "Failed to disconnect from libssh2 agent: %d %s", + rc, err_msg); + } + libssh2_agent_free(sshc->ssh_agent); + sshc->ssh_agent = NULL; + + /* NB: there is no need to free identities, they are part of internal + agent stuff */ + sshc->sshagent_identity = NULL; + sshc->sshagent_prev_identity = NULL; + } + + if(sshc->ssh_session) { + rc = libssh2_session_free(sshc->ssh_session); + if(!block && (rc == LIBSSH2_ERROR_EAGAIN)) { + return rc; + } + if(rc < 0) { + char *err_msg = NULL; + (void)libssh2_session_last_error(sshc->ssh_session, + &err_msg, NULL, 0); + infof(data, "Failed to free libssh2 session: %d %s", rc, err_msg); + } + sshc->ssh_session = NULL; + } + + /* worst-case scenario cleanup */ + DEBUGASSERT(sshc->ssh_session == NULL); + DEBUGASSERT(sshc->ssh_channel == NULL); + DEBUGASSERT(sshc->sftp_session == NULL); + DEBUGASSERT(sshc->sftp_handle == NULL); + DEBUGASSERT(sshc->kh == NULL); + DEBUGASSERT(sshc->ssh_agent == NULL); + + Curl_safefree(sshc->rsa_pub); + Curl_safefree(sshc->rsa); + Curl_safefree(sshc->quote_path1); + Curl_safefree(sshc->quote_path2); + Curl_safefree(sshc->homedir); + sshc->initialised = FALSE; + } + return 0; +} + + /* BLOCKING, but the function is using the state machine so the only reason this is still blocking is that the multi interface code has no support for disconnecting operations that takes a while */ @@ -3386,6 +3442,7 @@ static CURLcode scp_disconnect(struct Curl_easy *data, result = ssh_block_statemach(data, conn, TRUE); } + sshc_cleanup(sshc, data, TRUE); return result; } @@ -3405,6 +3462,7 @@ static CURLcode ssh_done(struct Curl_easy *data, CURLcode status) Curl_safefree(sshp->path); Curl_dyn_free(&sshp->readdir); + Curl_dyn_free(&sshp->readdir_link); if(Curl_pgrsDone(data)) return CURLE_ABORTED_BY_CALLBACK; @@ -3542,6 +3600,7 @@ static CURLcode sftp_disconnect(struct Curl_easy *data, } DEBUGF(infof(data, "SSH DISCONNECT is done")); + sshc_cleanup(sshc, data, TRUE); return result; -- cgit v1.2.3