http-backend.con commit Merge branch 'jc/doc-checkout' (4339c9f)
   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#include "packfile.h"
  13
  14static const char content_type[] = "Content-Type";
  15static const char content_length[] = "Content-Length";
  16static const char last_modified[] = "Last-Modified";
  17static int getanyfile = 1;
  18static unsigned long max_request_buffer = 10 * 1024 * 1024;
  19
  20static struct string_list *query_params;
  21
  22struct rpc_service {
  23        const char *name;
  24        const char *config_name;
  25        unsigned buffer_input : 1;
  26        signed enabled : 2;
  27};
  28
  29static struct rpc_service rpc_service[] = {
  30        { "upload-pack", "uploadpack", 1, 1 },
  31        { "receive-pack", "receivepack", 0, -1 },
  32};
  33
  34static struct string_list *get_parameters(void)
  35{
  36        if (!query_params) {
  37                const char *query = getenv("QUERY_STRING");
  38
  39                query_params = xcalloc(1, sizeof(*query_params));
  40                while (query && *query) {
  41                        char *name = url_decode_parameter_name(&query);
  42                        char *value = url_decode_parameter_value(&query);
  43                        struct string_list_item *i;
  44
  45                        i = string_list_lookup(query_params, name);
  46                        if (!i)
  47                                i = string_list_insert(query_params, name);
  48                        else
  49                                free(i->util);
  50                        i->util = value;
  51                }
  52        }
  53        return query_params;
  54}
  55
  56static const char *get_parameter(const char *name)
  57{
  58        struct string_list_item *i;
  59        i = string_list_lookup(get_parameters(), name);
  60        return i ? i->util : NULL;
  61}
  62
  63__attribute__((format (printf, 2, 3)))
  64static void format_write(int fd, const char *fmt, ...)
  65{
  66        static char buffer[1024];
  67
  68        va_list args;
  69        unsigned n;
  70
  71        va_start(args, fmt);
  72        n = vsnprintf(buffer, sizeof(buffer), fmt, args);
  73        va_end(args);
  74        if (n >= sizeof(buffer))
  75                die("protocol error: impossibly long line");
  76
  77        write_or_die(fd, buffer, n);
  78}
  79
  80static void http_status(struct strbuf *hdr, unsigned code, const char *msg)
  81{
  82        strbuf_addf(hdr, "Status: %u %s\r\n", code, msg);
  83}
  84
  85static void hdr_str(struct strbuf *hdr, const char *name, const char *value)
  86{
  87        strbuf_addf(hdr, "%s: %s\r\n", name, value);
  88}
  89
  90static void hdr_int(struct strbuf *hdr, const char *name, uintmax_t value)
  91{
  92        strbuf_addf(hdr, "%s: %" PRIuMAX "\r\n", name, value);
  93}
  94
  95static void hdr_date(struct strbuf *hdr, const char *name, timestamp_t when)
  96{
  97        const char *value = show_date(when, 0, DATE_MODE(RFC2822));
  98        hdr_str(hdr, name, value);
  99}
 100
 101static void hdr_nocache(struct strbuf *hdr)
 102{
 103        hdr_str(hdr, "Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
 104        hdr_str(hdr, "Pragma", "no-cache");
 105        hdr_str(hdr, "Cache-Control", "no-cache, max-age=0, must-revalidate");
 106}
 107
 108static void hdr_cache_forever(struct strbuf *hdr)
 109{
 110        timestamp_t now = time(NULL);
 111        hdr_date(hdr, "Date", now);
 112        hdr_date(hdr, "Expires", now + 31536000);
 113        hdr_str(hdr, "Cache-Control", "public, max-age=31536000");
 114}
 115
 116static void end_headers(struct strbuf *hdr)
 117{
 118        strbuf_add(hdr, "\r\n", 2);
 119        write_or_die(1, hdr->buf, hdr->len);
 120        strbuf_release(hdr);
 121}
 122
 123__attribute__((format (printf, 2, 3)))
 124static NORETURN void not_found(struct strbuf *hdr, const char *err, ...)
 125{
 126        va_list params;
 127
 128        http_status(hdr, 404, "Not Found");
 129        hdr_nocache(hdr);
 130        end_headers(hdr);
 131
 132        va_start(params, err);
 133        if (err && *err)
 134                vfprintf(stderr, err, params);
 135        va_end(params);
 136        exit(0);
 137}
 138
 139__attribute__((format (printf, 2, 3)))
 140static NORETURN void forbidden(struct strbuf *hdr, const char *err, ...)
 141{
 142        va_list params;
 143
 144        http_status(hdr, 403, "Forbidden");
 145        hdr_nocache(hdr);
 146        end_headers(hdr);
 147
 148        va_start(params, err);
 149        if (err && *err)
 150                vfprintf(stderr, err, params);
 151        va_end(params);
 152        exit(0);
 153}
 154
 155static void select_getanyfile(struct strbuf *hdr)
 156{
 157        if (!getanyfile)
 158                forbidden(hdr, "Unsupported service: getanyfile");
 159}
 160
 161static void send_strbuf(struct strbuf *hdr,
 162                        const char *type, struct strbuf *buf)
 163{
 164        hdr_int(hdr, content_length, buf->len);
 165        hdr_str(hdr, content_type, type);
 166        end_headers(hdr);
 167        write_or_die(1, buf->buf, buf->len);
 168}
 169
 170static void send_local_file(struct strbuf *hdr, const char *the_type,
 171                                const char *name)
 172{
 173        char *p = git_pathdup("%s", name);
 174        size_t buf_alloc = 8192;
 175        char *buf = xmalloc(buf_alloc);
 176        int fd;
 177        struct stat sb;
 178
 179        fd = open(p, O_RDONLY);
 180        if (fd < 0)
 181                not_found(hdr, "Cannot open '%s': %s", p, strerror(errno));
 182        if (fstat(fd, &sb) < 0)
 183                die_errno("Cannot stat '%s'", p);
 184
 185        hdr_int(hdr, content_length, sb.st_size);
 186        hdr_str(hdr, content_type, the_type);
 187        hdr_date(hdr, last_modified, sb.st_mtime);
 188        end_headers(hdr);
 189
 190        for (;;) {
 191                ssize_t n = xread(fd, buf, buf_alloc);
 192                if (n < 0)
 193                        die_errno("Cannot read '%s'", p);
 194                if (!n)
 195                        break;
 196                write_or_die(1, buf, n);
 197        }
 198        close(fd);
 199        free(buf);
 200        free(p);
 201}
 202
 203static void get_text_file(struct strbuf *hdr, char *name)
 204{
 205        select_getanyfile(hdr);
 206        hdr_nocache(hdr);
 207        send_local_file(hdr, "text/plain", name);
 208}
 209
 210static void get_loose_object(struct strbuf *hdr, char *name)
 211{
 212        select_getanyfile(hdr);
 213        hdr_cache_forever(hdr);
 214        send_local_file(hdr, "application/x-git-loose-object", name);
 215}
 216
 217static void get_pack_file(struct strbuf *hdr, char *name)
 218{
 219        select_getanyfile(hdr);
 220        hdr_cache_forever(hdr);
 221        send_local_file(hdr, "application/x-git-packed-objects", name);
 222}
 223
 224static void get_idx_file(struct strbuf *hdr, char *name)
 225{
 226        select_getanyfile(hdr);
 227        hdr_cache_forever(hdr);
 228        send_local_file(hdr, "application/x-git-packed-objects-toc", name);
 229}
 230
 231static void http_config(void)
 232{
 233        int i, value = 0;
 234        struct strbuf var = STRBUF_INIT;
 235
 236        git_config_get_bool("http.getanyfile", &getanyfile);
 237        git_config_get_ulong("http.maxrequestbuffer", &max_request_buffer);
 238
 239        for (i = 0; i < ARRAY_SIZE(rpc_service); i++) {
 240                struct rpc_service *svc = &rpc_service[i];
 241                strbuf_addf(&var, "http.%s", svc->config_name);
 242                if (!git_config_get_bool(var.buf, &value))
 243                        svc->enabled = value;
 244                strbuf_reset(&var);
 245        }
 246
 247        strbuf_release(&var);
 248}
 249
 250static struct rpc_service *select_service(struct strbuf *hdr, const char *name)
 251{
 252        const char *svc_name;
 253        struct rpc_service *svc = NULL;
 254        int i;
 255
 256        if (!skip_prefix(name, "git-", &svc_name))
 257                forbidden(hdr, "Unsupported service: '%s'", name);
 258
 259        for (i = 0; i < ARRAY_SIZE(rpc_service); i++) {
 260                struct rpc_service *s = &rpc_service[i];
 261                if (!strcmp(s->name, svc_name)) {
 262                        svc = s;
 263                        break;
 264                }
 265        }
 266
 267        if (!svc)
 268                forbidden(hdr, "Unsupported service: '%s'", name);
 269
 270        if (svc->enabled < 0) {
 271                const char *user = getenv("REMOTE_USER");
 272                svc->enabled = (user && *user) ? 1 : 0;
 273        }
 274        if (!svc->enabled)
 275                forbidden(hdr, "Service not enabled: '%s'", svc->name);
 276        return svc;
 277}
 278
 279/*
 280 * This is basically strbuf_read(), except that if we
 281 * hit max_request_buffer we die (we'd rather reject a
 282 * maliciously large request than chew up infinite memory).
 283 */
 284static ssize_t read_request(int fd, unsigned char **out)
 285{
 286        size_t len = 0, alloc = 8192;
 287        unsigned char *buf = xmalloc(alloc);
 288
 289        if (max_request_buffer < alloc)
 290                max_request_buffer = alloc;
 291
 292        while (1) {
 293                ssize_t cnt;
 294
 295                cnt = read_in_full(fd, buf + len, alloc - len);
 296                if (cnt < 0) {
 297                        free(buf);
 298                        return -1;
 299                }
 300
 301                /* partial read from read_in_full means we hit EOF */
 302                len += cnt;
 303                if (len < alloc) {
 304                        *out = buf;
 305                        return len;
 306                }
 307
 308                /* otherwise, grow and try again (if we can) */
 309                if (alloc == max_request_buffer)
 310                        die("request was larger than our maximum size (%lu);"
 311                            " try setting GIT_HTTP_MAX_REQUEST_BUFFER",
 312                            max_request_buffer);
 313
 314                alloc = alloc_nr(alloc);
 315                if (alloc > max_request_buffer)
 316                        alloc = max_request_buffer;
 317                REALLOC_ARRAY(buf, alloc);
 318        }
 319}
 320
 321static void inflate_request(const char *prog_name, int out, int buffer_input)
 322{
 323        git_zstream stream;
 324        unsigned char *full_request = NULL;
 325        unsigned char in_buf[8192];
 326        unsigned char out_buf[8192];
 327        unsigned long cnt = 0;
 328
 329        memset(&stream, 0, sizeof(stream));
 330        git_inflate_init_gzip_only(&stream);
 331
 332        while (1) {
 333                ssize_t n;
 334
 335                if (buffer_input) {
 336                        if (full_request)
 337                                n = 0; /* nothing left to read */
 338                        else
 339                                n = read_request(0, &full_request);
 340                        stream.next_in = full_request;
 341                } else {
 342                        n = xread(0, in_buf, sizeof(in_buf));
 343                        stream.next_in = in_buf;
 344                }
 345
 346                if (n <= 0)
 347                        die("request ended in the middle of the gzip stream");
 348                stream.avail_in = n;
 349
 350                while (0 < stream.avail_in) {
 351                        int ret;
 352
 353                        stream.next_out = out_buf;
 354                        stream.avail_out = sizeof(out_buf);
 355
 356                        ret = git_inflate(&stream, Z_NO_FLUSH);
 357                        if (ret != Z_OK && ret != Z_STREAM_END)
 358                                die("zlib error inflating request, result %d", ret);
 359
 360                        n = stream.total_out - cnt;
 361                        if (write_in_full(out, out_buf, n) < 0)
 362                                die("%s aborted reading request", prog_name);
 363                        cnt += n;
 364
 365                        if (ret == Z_STREAM_END)
 366                                goto done;
 367                }
 368        }
 369
 370done:
 371        git_inflate_end(&stream);
 372        close(out);
 373        free(full_request);
 374}
 375
 376static void copy_request(const char *prog_name, int out)
 377{
 378        unsigned char *buf;
 379        ssize_t n = read_request(0, &buf);
 380        if (n < 0)
 381                die_errno("error reading request body");
 382        if (write_in_full(out, buf, n) < 0)
 383                die("%s aborted reading request", prog_name);
 384        close(out);
 385        free(buf);
 386}
 387
 388static void run_service(const char **argv, int buffer_input)
 389{
 390        const char *encoding = getenv("HTTP_CONTENT_ENCODING");
 391        const char *user = getenv("REMOTE_USER");
 392        const char *host = getenv("REMOTE_ADDR");
 393        int gzipped_request = 0;
 394        struct child_process cld = CHILD_PROCESS_INIT;
 395
 396        if (encoding && !strcmp(encoding, "gzip"))
 397                gzipped_request = 1;
 398        else if (encoding && !strcmp(encoding, "x-gzip"))
 399                gzipped_request = 1;
 400
 401        if (!user || !*user)
 402                user = "anonymous";
 403        if (!host || !*host)
 404                host = "(none)";
 405
 406        if (!getenv("GIT_COMMITTER_NAME"))
 407                argv_array_pushf(&cld.env_array, "GIT_COMMITTER_NAME=%s", user);
 408        if (!getenv("GIT_COMMITTER_EMAIL"))
 409                argv_array_pushf(&cld.env_array,
 410                                 "GIT_COMMITTER_EMAIL=%s@http.%s", user, host);
 411
 412        cld.argv = argv;
 413        if (buffer_input || gzipped_request)
 414                cld.in = -1;
 415        cld.git_cmd = 1;
 416        if (start_command(&cld))
 417                exit(1);
 418
 419        close(1);
 420        if (gzipped_request)
 421                inflate_request(argv[0], cld.in, buffer_input);
 422        else if (buffer_input)
 423                copy_request(argv[0], cld.in);
 424        else
 425                close(0);
 426
 427        if (finish_command(&cld))
 428                exit(1);
 429}
 430
 431static int show_text_ref(const char *name, const struct object_id *oid,
 432                         int flag, void *cb_data)
 433{
 434        const char *name_nons = strip_namespace(name);
 435        struct strbuf *buf = cb_data;
 436        struct object *o = parse_object(oid);
 437        if (!o)
 438                return 0;
 439
 440        strbuf_addf(buf, "%s\t%s\n", oid_to_hex(oid), name_nons);
 441        if (o->type == OBJ_TAG) {
 442                o = deref_tag(o, name, 0);
 443                if (!o)
 444                        return 0;
 445                strbuf_addf(buf, "%s\t%s^{}\n", oid_to_hex(&o->oid),
 446                            name_nons);
 447        }
 448        return 0;
 449}
 450
 451static void get_info_refs(struct strbuf *hdr, char *arg)
 452{
 453        const char *service_name = get_parameter("service");
 454        struct strbuf buf = STRBUF_INIT;
 455
 456        hdr_nocache(hdr);
 457
 458        if (service_name) {
 459                const char *argv[] = {NULL /* service name */,
 460                        "--stateless-rpc", "--advertise-refs",
 461                        ".", NULL};
 462                struct rpc_service *svc = select_service(hdr, service_name);
 463
 464                strbuf_addf(&buf, "application/x-git-%s-advertisement",
 465                        svc->name);
 466                hdr_str(hdr, content_type, buf.buf);
 467                end_headers(hdr);
 468
 469                packet_write_fmt(1, "# service=git-%s\n", svc->name);
 470                packet_flush(1);
 471
 472                argv[0] = svc->name;
 473                run_service(argv, 0);
 474
 475        } else {
 476                select_getanyfile(hdr);
 477                for_each_namespaced_ref(show_text_ref, &buf);
 478                send_strbuf(hdr, "text/plain", &buf);
 479        }
 480        strbuf_release(&buf);
 481}
 482
 483static int show_head_ref(const char *refname, const struct object_id *oid,
 484                         int flag, void *cb_data)
 485{
 486        struct strbuf *buf = cb_data;
 487
 488        if (flag & REF_ISSYMREF) {
 489                const char *target = resolve_ref_unsafe(refname,
 490                                                        RESOLVE_REF_READING,
 491                                                        NULL, 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}