http-backend.con commit grep: recurse in-process using 'struct repository' (f9ee2fc)
   1#include "cache.h"
   2#include "config.h"
   3#include "refs.h"
   4#include "pkt-line.h"
   5#include "object.h"
   6#include "tag.h"
   7#include "exec_cmd.h"
   8#include "run-command.h"
   9#include "string-list.h"
  10#include "url.h"
  11#include "argv-array.h"
  12
  13static const char content_type[] = "Content-Type";
  14static const char content_length[] = "Content-Length";
  15static const char last_modified[] = "Last-Modified";
  16static int getanyfile = 1;
  17static unsigned long max_request_buffer = 10 * 1024 * 1024;
  18
  19static struct string_list *query_params;
  20
  21struct rpc_service {
  22        const char *name;
  23        const char *config_name;
  24        unsigned buffer_input : 1;
  25        signed enabled : 2;
  26};
  27
  28static struct rpc_service rpc_service[] = {
  29        { "upload-pack", "uploadpack", 1, 1 },
  30        { "receive-pack", "receivepack", 0, -1 },
  31};
  32
  33static struct string_list *get_parameters(void)
  34{
  35        if (!query_params) {
  36                const char *query = getenv("QUERY_STRING");
  37
  38                query_params = xcalloc(1, sizeof(*query_params));
  39                while (query && *query) {
  40                        char *name = url_decode_parameter_name(&query);
  41                        char *value = url_decode_parameter_value(&query);
  42                        struct string_list_item *i;
  43
  44                        i = string_list_lookup(query_params, name);
  45                        if (!i)
  46                                i = string_list_insert(query_params, name);
  47                        else
  48                                free(i->util);
  49                        i->util = value;
  50                }
  51        }
  52        return query_params;
  53}
  54
  55static const char *get_parameter(const char *name)
  56{
  57        struct string_list_item *i;
  58        i = string_list_lookup(get_parameters(), name);
  59        return i ? i->util : NULL;
  60}
  61
  62__attribute__((format (printf, 2, 3)))
  63static void format_write(int fd, const char *fmt, ...)
  64{
  65        static char buffer[1024];
  66
  67        va_list args;
  68        unsigned n;
  69
  70        va_start(args, fmt);
  71        n = vsnprintf(buffer, sizeof(buffer), fmt, args);
  72        va_end(args);
  73        if (n >= sizeof(buffer))
  74                die("protocol error: impossibly long line");
  75
  76        write_or_die(fd, buffer, n);
  77}
  78
  79static void http_status(struct strbuf *hdr, unsigned code, const char *msg)
  80{
  81        strbuf_addf(hdr, "Status: %u %s\r\n", code, msg);
  82}
  83
  84static void hdr_str(struct strbuf *hdr, const char *name, const char *value)
  85{
  86        strbuf_addf(hdr, "%s: %s\r\n", name, value);
  87}
  88
  89static void hdr_int(struct strbuf *hdr, const char *name, uintmax_t value)
  90{
  91        strbuf_addf(hdr, "%s: %" PRIuMAX "\r\n", name, value);
  92}
  93
  94static void hdr_date(struct strbuf *hdr, const char *name, timestamp_t when)
  95{
  96        const char *value = show_date(when, 0, DATE_MODE(RFC2822));
  97        hdr_str(hdr, name, value);
  98}
  99
 100static void hdr_nocache(struct strbuf *hdr)
 101{
 102        hdr_str(hdr, "Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
 103        hdr_str(hdr, "Pragma", "no-cache");
 104        hdr_str(hdr, "Cache-Control", "no-cache, max-age=0, must-revalidate");
 105}
 106
 107static void hdr_cache_forever(struct strbuf *hdr)
 108{
 109        timestamp_t now = time(NULL);
 110        hdr_date(hdr, "Date", now);
 111        hdr_date(hdr, "Expires", now + 31536000);
 112        hdr_str(hdr, "Cache-Control", "public, max-age=31536000");
 113}
 114
 115static void end_headers(struct strbuf *hdr)
 116{
 117        strbuf_add(hdr, "\r\n", 2);
 118        write_or_die(1, hdr->buf, hdr->len);
 119        strbuf_release(hdr);
 120}
 121
 122__attribute__((format (printf, 2, 3)))
 123static NORETURN void not_found(struct strbuf *hdr, const char *err, ...)
 124{
 125        va_list params;
 126
 127        http_status(hdr, 404, "Not Found");
 128        hdr_nocache(hdr);
 129        end_headers(hdr);
 130
 131        va_start(params, err);
 132        if (err && *err)
 133                vfprintf(stderr, err, params);
 134        va_end(params);
 135        exit(0);
 136}
 137
 138__attribute__((format (printf, 2, 3)))
 139static NORETURN void forbidden(struct strbuf *hdr, const char *err, ...)
 140{
 141        va_list params;
 142
 143        http_status(hdr, 403, "Forbidden");
 144        hdr_nocache(hdr);
 145        end_headers(hdr);
 146
 147        va_start(params, err);
 148        if (err && *err)
 149                vfprintf(stderr, err, params);
 150        va_end(params);
 151        exit(0);
 152}
 153
 154static void select_getanyfile(struct strbuf *hdr)
 155{
 156        if (!getanyfile)
 157                forbidden(hdr, "Unsupported service: getanyfile");
 158}
 159
 160static void send_strbuf(struct strbuf *hdr,
 161                        const char *type, struct strbuf *buf)
 162{
 163        hdr_int(hdr, content_length, buf->len);
 164        hdr_str(hdr, content_type, type);
 165        end_headers(hdr);
 166        write_or_die(1, buf->buf, buf->len);
 167}
 168
 169static void send_local_file(struct strbuf *hdr, const char *the_type,
 170                                const char *name)
 171{
 172        char *p = git_pathdup("%s", name);
 173        size_t buf_alloc = 8192;
 174        char *buf = xmalloc(buf_alloc);
 175        int fd;
 176        struct stat sb;
 177
 178        fd = open(p, O_RDONLY);
 179        if (fd < 0)
 180                not_found(hdr, "Cannot open '%s': %s", p, strerror(errno));
 181        if (fstat(fd, &sb) < 0)
 182                die_errno("Cannot stat '%s'", p);
 183
 184        hdr_int(hdr, content_length, sb.st_size);
 185        hdr_str(hdr, content_type, the_type);
 186        hdr_date(hdr, last_modified, sb.st_mtime);
 187        end_headers(hdr);
 188
 189        for (;;) {
 190                ssize_t n = xread(fd, buf, buf_alloc);
 191                if (n < 0)
 192                        die_errno("Cannot read '%s'", p);
 193                if (!n)
 194                        break;
 195                write_or_die(1, buf, n);
 196        }
 197        close(fd);
 198        free(buf);
 199        free(p);
 200}
 201
 202static void get_text_file(struct strbuf *hdr, char *name)
 203{
 204        select_getanyfile(hdr);
 205        hdr_nocache(hdr);
 206        send_local_file(hdr, "text/plain", name);
 207}
 208
 209static void get_loose_object(struct strbuf *hdr, char *name)
 210{
 211        select_getanyfile(hdr);
 212        hdr_cache_forever(hdr);
 213        send_local_file(hdr, "application/x-git-loose-object", name);
 214}
 215
 216static void get_pack_file(struct strbuf *hdr, char *name)
 217{
 218        select_getanyfile(hdr);
 219        hdr_cache_forever(hdr);
 220        send_local_file(hdr, "application/x-git-packed-objects", name);
 221}
 222
 223static void get_idx_file(struct strbuf *hdr, char *name)
 224{
 225        select_getanyfile(hdr);
 226        hdr_cache_forever(hdr);
 227        send_local_file(hdr, "application/x-git-packed-objects-toc", name);
 228}
 229
 230static void http_config(void)
 231{
 232        int i, value = 0;
 233        struct strbuf var = STRBUF_INIT;
 234
 235        git_config_get_bool("http.getanyfile", &getanyfile);
 236        git_config_get_ulong("http.maxrequestbuffer", &max_request_buffer);
 237
 238        for (i = 0; i < ARRAY_SIZE(rpc_service); i++) {
 239                struct rpc_service *svc = &rpc_service[i];
 240                strbuf_addf(&var, "http.%s", svc->config_name);
 241                if (!git_config_get_bool(var.buf, &value))
 242                        svc->enabled = value;
 243                strbuf_reset(&var);
 244        }
 245
 246        strbuf_release(&var);
 247}
 248
 249static struct rpc_service *select_service(struct strbuf *hdr, const char *name)
 250{
 251        const char *svc_name;
 252        struct rpc_service *svc = NULL;
 253        int i;
 254
 255        if (!skip_prefix(name, "git-", &svc_name))
 256                forbidden(hdr, "Unsupported service: '%s'", name);
 257
 258        for (i = 0; i < ARRAY_SIZE(rpc_service); i++) {
 259                struct rpc_service *s = &rpc_service[i];
 260                if (!strcmp(s->name, svc_name)) {
 261                        svc = s;
 262                        break;
 263                }
 264        }
 265
 266        if (!svc)
 267                forbidden(hdr, "Unsupported service: '%s'", name);
 268
 269        if (svc->enabled < 0) {
 270                const char *user = getenv("REMOTE_USER");
 271                svc->enabled = (user && *user) ? 1 : 0;
 272        }
 273        if (!svc->enabled)
 274                forbidden(hdr, "Service not enabled: '%s'", svc->name);
 275        return svc;
 276}
 277
 278/*
 279 * This is basically strbuf_read(), except that if we
 280 * hit max_request_buffer we die (we'd rather reject a
 281 * maliciously large request than chew up infinite memory).
 282 */
 283static ssize_t read_request(int fd, unsigned char **out)
 284{
 285        size_t len = 0, alloc = 8192;
 286        unsigned char *buf = xmalloc(alloc);
 287
 288        if (max_request_buffer < alloc)
 289                max_request_buffer = alloc;
 290
 291        while (1) {
 292                ssize_t cnt;
 293
 294                cnt = read_in_full(fd, buf + len, alloc - len);
 295                if (cnt < 0) {
 296                        free(buf);
 297                        return -1;
 298                }
 299
 300                /* partial read from read_in_full means we hit EOF */
 301                len += cnt;
 302                if (len < alloc) {
 303                        *out = buf;
 304                        return len;
 305                }
 306
 307                /* otherwise, grow and try again (if we can) */
 308                if (alloc == max_request_buffer)
 309                        die("request was larger than our maximum size (%lu);"
 310                            " try setting GIT_HTTP_MAX_REQUEST_BUFFER",
 311                            max_request_buffer);
 312
 313                alloc = alloc_nr(alloc);
 314                if (alloc > max_request_buffer)
 315                        alloc = max_request_buffer;
 316                REALLOC_ARRAY(buf, alloc);
 317        }
 318}
 319
 320static void inflate_request(const char *prog_name, int out, int buffer_input)
 321{
 322        git_zstream stream;
 323        unsigned char *full_request = NULL;
 324        unsigned char in_buf[8192];
 325        unsigned char out_buf[8192];
 326        unsigned long cnt = 0;
 327
 328        memset(&stream, 0, sizeof(stream));
 329        git_inflate_init_gzip_only(&stream);
 330
 331        while (1) {
 332                ssize_t n;
 333
 334                if (buffer_input) {
 335                        if (full_request)
 336                                n = 0; /* nothing left to read */
 337                        else
 338                                n = read_request(0, &full_request);
 339                        stream.next_in = full_request;
 340                } else {
 341                        n = xread(0, in_buf, sizeof(in_buf));
 342                        stream.next_in = in_buf;
 343                }
 344
 345                if (n <= 0)
 346                        die("request ended in the middle of the gzip stream");
 347                stream.avail_in = n;
 348
 349                while (0 < stream.avail_in) {
 350                        int ret;
 351
 352                        stream.next_out = out_buf;
 353                        stream.avail_out = sizeof(out_buf);
 354
 355                        ret = git_inflate(&stream, Z_NO_FLUSH);
 356                        if (ret != Z_OK && ret != Z_STREAM_END)
 357                                die("zlib error inflating request, result %d", ret);
 358
 359                        n = stream.total_out - cnt;
 360                        if (write_in_full(out, out_buf, n) != n)
 361                                die("%s aborted reading request", prog_name);
 362                        cnt += n;
 363
 364                        if (ret == Z_STREAM_END)
 365                                goto done;
 366                }
 367        }
 368
 369done:
 370        git_inflate_end(&stream);
 371        close(out);
 372        free(full_request);
 373}
 374
 375static void copy_request(const char *prog_name, int out)
 376{
 377        unsigned char *buf;
 378        ssize_t n = read_request(0, &buf);
 379        if (n < 0)
 380                die_errno("error reading request body");
 381        if (write_in_full(out, buf, n) != n)
 382                die("%s aborted reading request", prog_name);
 383        close(out);
 384        free(buf);
 385}
 386
 387static void run_service(const char **argv, int buffer_input)
 388{
 389        const char *encoding = getenv("HTTP_CONTENT_ENCODING");
 390        const char *user = getenv("REMOTE_USER");
 391        const char *host = getenv("REMOTE_ADDR");
 392        int gzipped_request = 0;
 393        struct child_process cld = CHILD_PROCESS_INIT;
 394
 395        if (encoding && !strcmp(encoding, "gzip"))
 396                gzipped_request = 1;
 397        else if (encoding && !strcmp(encoding, "x-gzip"))
 398                gzipped_request = 1;
 399
 400        if (!user || !*user)
 401                user = "anonymous";
 402        if (!host || !*host)
 403                host = "(none)";
 404
 405        if (!getenv("GIT_COMMITTER_NAME"))
 406                argv_array_pushf(&cld.env_array, "GIT_COMMITTER_NAME=%s", user);
 407        if (!getenv("GIT_COMMITTER_EMAIL"))
 408                argv_array_pushf(&cld.env_array,
 409                                 "GIT_COMMITTER_EMAIL=%s@http.%s", user, host);
 410
 411        cld.argv = argv;
 412        if (buffer_input || gzipped_request)
 413                cld.in = -1;
 414        cld.git_cmd = 1;
 415        if (start_command(&cld))
 416                exit(1);
 417
 418        close(1);
 419        if (gzipped_request)
 420                inflate_request(argv[0], cld.in, buffer_input);
 421        else if (buffer_input)
 422                copy_request(argv[0], cld.in);
 423        else
 424                close(0);
 425
 426        if (finish_command(&cld))
 427                exit(1);
 428}
 429
 430static int show_text_ref(const char *name, const struct object_id *oid,
 431                         int flag, void *cb_data)
 432{
 433        const char *name_nons = strip_namespace(name);
 434        struct strbuf *buf = cb_data;
 435        struct object *o = parse_object(oid);
 436        if (!o)
 437                return 0;
 438
 439        strbuf_addf(buf, "%s\t%s\n", oid_to_hex(oid), name_nons);
 440        if (o->type == OBJ_TAG) {
 441                o = deref_tag(o, name, 0);
 442                if (!o)
 443                        return 0;
 444                strbuf_addf(buf, "%s\t%s^{}\n", oid_to_hex(&o->oid),
 445                            name_nons);
 446        }
 447        return 0;
 448}
 449
 450static void get_info_refs(struct strbuf *hdr, char *arg)
 451{
 452        const char *service_name = get_parameter("service");
 453        struct strbuf buf = STRBUF_INIT;
 454
 455        hdr_nocache(hdr);
 456
 457        if (service_name) {
 458                const char *argv[] = {NULL /* service name */,
 459                        "--stateless-rpc", "--advertise-refs",
 460                        ".", NULL};
 461                struct rpc_service *svc = select_service(hdr, service_name);
 462
 463                strbuf_addf(&buf, "application/x-git-%s-advertisement",
 464                        svc->name);
 465                hdr_str(hdr, content_type, buf.buf);
 466                end_headers(hdr);
 467
 468                packet_write_fmt(1, "# service=git-%s\n", svc->name);
 469                packet_flush(1);
 470
 471                argv[0] = svc->name;
 472                run_service(argv, 0);
 473
 474        } else {
 475                select_getanyfile(hdr);
 476                for_each_namespaced_ref(show_text_ref, &buf);
 477                send_strbuf(hdr, "text/plain", &buf);
 478        }
 479        strbuf_release(&buf);
 480}
 481
 482static int show_head_ref(const char *refname, const struct object_id *oid,
 483                         int flag, void *cb_data)
 484{
 485        struct strbuf *buf = cb_data;
 486
 487        if (flag & REF_ISSYMREF) {
 488                struct object_id unused;
 489                const char *target = resolve_ref_unsafe(refname,
 490                                                        RESOLVE_REF_READING,
 491                                                        unused.hash, NULL);
 492
 493                if (target)
 494                        strbuf_addf(buf, "ref: %s\n", strip_namespace(target));
 495        } else {
 496                strbuf_addf(buf, "%s\n", oid_to_hex(oid));
 497        }
 498
 499        return 0;
 500}
 501
 502static void get_head(struct strbuf *hdr, char *arg)
 503{
 504        struct strbuf buf = STRBUF_INIT;
 505
 506        select_getanyfile(hdr);
 507        head_ref_namespaced(show_head_ref, &buf);
 508        send_strbuf(hdr, "text/plain", &buf);
 509        strbuf_release(&buf);
 510}
 511
 512static void get_info_packs(struct strbuf *hdr, char *arg)
 513{
 514        size_t objdirlen = strlen(get_object_directory());
 515        struct strbuf buf = STRBUF_INIT;
 516        struct packed_git *p;
 517        size_t cnt = 0;
 518
 519        select_getanyfile(hdr);
 520        prepare_packed_git();
 521        for (p = packed_git; p; p = p->next) {
 522                if (p->pack_local)
 523                        cnt++;
 524        }
 525
 526        strbuf_grow(&buf, cnt * 53 + 2);
 527        for (p = packed_git; p; p = p->next) {
 528                if (p->pack_local)
 529                        strbuf_addf(&buf, "P %s\n", p->pack_name + objdirlen + 6);
 530        }
 531        strbuf_addch(&buf, '\n');
 532
 533        hdr_nocache(hdr);
 534        send_strbuf(hdr, "text/plain; charset=utf-8", &buf);
 535        strbuf_release(&buf);
 536}
 537
 538static void check_content_type(struct strbuf *hdr, const char *accepted_type)
 539{
 540        const char *actual_type = getenv("CONTENT_TYPE");
 541
 542        if (!actual_type)
 543                actual_type = "";
 544
 545        if (strcmp(actual_type, accepted_type)) {
 546                http_status(hdr, 415, "Unsupported Media Type");
 547                hdr_nocache(hdr);
 548                end_headers(hdr);
 549                format_write(1,
 550                        "Expected POST with Content-Type '%s',"
 551                        " but received '%s' instead.\n",
 552                        accepted_type, actual_type);
 553                exit(0);
 554        }
 555}
 556
 557static void service_rpc(struct strbuf *hdr, char *service_name)
 558{
 559        const char *argv[] = {NULL, "--stateless-rpc", ".", NULL};
 560        struct rpc_service *svc = select_service(hdr, service_name);
 561        struct strbuf buf = STRBUF_INIT;
 562
 563        strbuf_reset(&buf);
 564        strbuf_addf(&buf, "application/x-git-%s-request", svc->name);
 565        check_content_type(hdr, buf.buf);
 566
 567        hdr_nocache(hdr);
 568
 569        strbuf_reset(&buf);
 570        strbuf_addf(&buf, "application/x-git-%s-result", svc->name);
 571        hdr_str(hdr, content_type, buf.buf);
 572
 573        end_headers(hdr);
 574
 575        argv[0] = svc->name;
 576        run_service(argv, svc->buffer_input);
 577        strbuf_release(&buf);
 578}
 579
 580static int dead;
 581static NORETURN void die_webcgi(const char *err, va_list params)
 582{
 583        if (dead <= 1) {
 584                struct strbuf hdr = STRBUF_INIT;
 585
 586                vreportf("fatal: ", err, params);
 587
 588                http_status(&hdr, 500, "Internal Server Error");
 589                hdr_nocache(&hdr);
 590                end_headers(&hdr);
 591        }
 592        exit(0); /* we successfully reported a failure ;-) */
 593}
 594
 595static int die_webcgi_recursing(void)
 596{
 597        return dead++ > 1;
 598}
 599
 600static char* getdir(void)
 601{
 602        struct strbuf buf = STRBUF_INIT;
 603        char *pathinfo = getenv("PATH_INFO");
 604        char *root = getenv("GIT_PROJECT_ROOT");
 605        char *path = getenv("PATH_TRANSLATED");
 606
 607        if (root && *root) {
 608                if (!pathinfo || !*pathinfo)
 609                        die("GIT_PROJECT_ROOT is set but PATH_INFO is not");
 610                if (daemon_avoid_alias(pathinfo))
 611                        die("'%s': aliased", pathinfo);
 612                end_url_with_slash(&buf, root);
 613                if (pathinfo[0] == '/')
 614                        pathinfo++;
 615                strbuf_addstr(&buf, pathinfo);
 616                return strbuf_detach(&buf, NULL);
 617        } else if (path && *path) {
 618                return xstrdup(path);
 619        } else
 620                die("No GIT_PROJECT_ROOT or PATH_TRANSLATED from server");
 621        return NULL;
 622}
 623
 624static struct service_cmd {
 625        const char *method;
 626        const char *pattern;
 627        void (*imp)(struct strbuf *, char *);
 628} services[] = {
 629        {"GET", "/HEAD$", get_head},
 630        {"GET", "/info/refs$", get_info_refs},
 631        {"GET", "/objects/info/alternates$", get_text_file},
 632        {"GET", "/objects/info/http-alternates$", get_text_file},
 633        {"GET", "/objects/info/packs$", get_info_packs},
 634        {"GET", "/objects/[0-9a-f]{2}/[0-9a-f]{38}$", get_loose_object},
 635        {"GET", "/objects/pack/pack-[0-9a-f]{40}\\.pack$", get_pack_file},
 636        {"GET", "/objects/pack/pack-[0-9a-f]{40}\\.idx$", get_idx_file},
 637
 638        {"POST", "/git-upload-pack$", service_rpc},
 639        {"POST", "/git-receive-pack$", service_rpc}
 640};
 641
 642static int bad_request(struct strbuf *hdr, const struct service_cmd *c)
 643{
 644        const char *proto = getenv("SERVER_PROTOCOL");
 645
 646        if (proto && !strcmp(proto, "HTTP/1.1")) {
 647                http_status(hdr, 405, "Method Not Allowed");
 648                hdr_str(hdr, "Allow",
 649                        !strcmp(c->method, "GET") ? "GET, HEAD" : c->method);
 650        } else
 651                http_status(hdr, 400, "Bad Request");
 652        hdr_nocache(hdr);
 653        end_headers(hdr);
 654        return 0;
 655}
 656
 657int cmd_main(int argc, const char **argv)
 658{
 659        char *method = getenv("REQUEST_METHOD");
 660        char *dir;
 661        struct service_cmd *cmd = NULL;
 662        char *cmd_arg = NULL;
 663        int i;
 664        struct strbuf hdr = STRBUF_INIT;
 665
 666        set_die_routine(die_webcgi);
 667        set_die_is_recursing_routine(die_webcgi_recursing);
 668
 669        if (!method)
 670                die("No REQUEST_METHOD from server");
 671        if (!strcmp(method, "HEAD"))
 672                method = "GET";
 673        dir = getdir();
 674
 675        for (i = 0; i < ARRAY_SIZE(services); i++) {
 676                struct service_cmd *c = &services[i];
 677                regex_t re;
 678                regmatch_t out[1];
 679
 680                if (regcomp(&re, c->pattern, REG_EXTENDED))
 681                        die("Bogus regex in service table: %s", c->pattern);
 682                if (!regexec(&re, dir, 1, out, 0)) {
 683                        size_t n;
 684
 685                        if (strcmp(method, c->method))
 686                                return bad_request(&hdr, c);
 687
 688                        cmd = c;
 689                        n = out[0].rm_eo - out[0].rm_so;
 690                        cmd_arg = xmemdupz(dir + out[0].rm_so + 1, n - 1);
 691                        dir[out[0].rm_so] = 0;
 692                        break;
 693                }
 694                regfree(&re);
 695        }
 696
 697        if (!cmd)
 698                not_found(&hdr, "Request not supported: '%s'", dir);
 699
 700        setup_path();
 701        if (!enter_repo(dir, 0))
 702                not_found(&hdr, "Not a git repository: '%s'", dir);
 703        if (!getenv("GIT_HTTP_EXPORT_ALL") &&
 704            access("git-daemon-export-ok", F_OK) )
 705                not_found(&hdr, "Repository not exported: '%s'", dir);
 706
 707        http_config();
 708        max_request_buffer = git_env_ulong("GIT_HTTP_MAX_REQUEST_BUFFER",
 709                                           max_request_buffer);
 710
 711        cmd->imp(&hdr, cmd_arg);
 712        return 0;
 713}