1 /* $OpenBSD: main.c,v 1.72 2025/01/16 14:06:49 job Exp $ */ 2 /* 3 * Copyright (c) 2019 Kristaps Dzonsons <kristaps@bsd.lv> 4 * 5 * Permission to use, copy, modify, and distribute this software for any 6 * purpose with or without fee is hereby granted, provided that the above 7 * copyright notice and this permission notice appear in all copies. 8 * 9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 */ 17 #include <sys/stat.h> 18 #include <sys/socket.h> 19 #include <sys/wait.h> 20 21 #include <assert.h> 22 #include <err.h> 23 #include <getopt.h> 24 #include <stdint.h> 25 #include <stdio.h> 26 #include <stdlib.h> 27 #include <string.h> 28 #include <unistd.h> 29 #include <util.h> 30 31 #include "extern.h" 32 #include "version.h" 33 34 int verbose; 35 int poll_contimeout; 36 int poll_timeout; 37 38 /* 39 * A remote host is has a colon before the first path separator. 40 * This works for rsh remote hosts (host:/foo/bar), implicit rsync 41 * remote hosts (host::/foo/bar), and explicit (rsync://host/foo). 42 * Return zero if local, non-zero if remote. 43 */ 44 static int 45 fargs_is_remote(const char *v) 46 { 47 size_t pos; 48 49 pos = strcspn(v, ":/"); 50 return v[pos] == ':'; 51 } 52 53 /* 54 * Test whether a remote host is specifically an rsync daemon. 55 * Return zero if not, non-zero if so. 56 */ 57 static int 58 fargs_is_daemon(const char *v) 59 { 60 size_t pos; 61 62 if (strncasecmp(v, "rsync://", 8) == 0) 63 return 1; 64 65 pos = strcspn(v, ":/"); 66 return v[pos] == ':' && v[pos + 1] == ':'; 67 } 68 69 /* 70 * Take the command-line filenames (e.g., rsync foo/ bar/ baz/) and 71 * determine our operating mode. 72 * For example, if the first argument is a remote file, this means that 73 * we're going to transfer from the remote to the local. 74 * We also make sure that the arguments are consistent, that is, if 75 * we're going to transfer from the local to the remote, that no 76 * filenames for the local transfer indicate remote hosts. 77 * Always returns the parsed and sanitised options. 78 */ 79 static struct fargs * 80 fargs_parse(size_t argc, char *argv[], struct opts *opts) 81 { 82 struct fargs *f = NULL; 83 char *cp, *ccp; 84 size_t i, j, len = 0; 85 86 /* Allocations. */ 87 88 if ((f = calloc(1, sizeof(struct fargs))) == NULL) 89 err(ERR_NOMEM, NULL); 90 91 f->sourcesz = argc - 1; 92 if ((f->sources = calloc(f->sourcesz, sizeof(char *))) == NULL) 93 err(ERR_NOMEM, NULL); 94 95 for (i = 0; i < argc - 1; i++) 96 if ((f->sources[i] = strdup(argv[i])) == NULL) 97 err(ERR_NOMEM, NULL); 98 99 if ((f->sink = strdup(argv[i])) == NULL) 100 err(ERR_NOMEM, NULL); 101 102 /* 103 * Test files for its locality. 104 * If the last is a remote host, then we're sending from the 105 * local to the remote host ("sender" mode). 106 * If the first, remote to local ("receiver" mode). 107 * If neither, a local transfer in sender style. 108 */ 109 110 f->mode = FARGS_SENDER; 111 112 if (fargs_is_remote(f->sink)) { 113 f->mode = FARGS_SENDER; 114 if ((f->host = strdup(f->sink)) == NULL) 115 err(ERR_NOMEM, NULL); 116 } 117 118 if (fargs_is_remote(f->sources[0])) { 119 if (f->host != NULL) 120 errx(ERR_SYNTAX, "both source and destination " 121 "cannot be remote files"); 122 f->mode = FARGS_RECEIVER; 123 if ((f->host = strdup(f->sources[0])) == NULL) 124 err(ERR_NOMEM, NULL); 125 } 126 127 if (f->host != NULL) { 128 if (strncasecmp(f->host, "rsync://", 8) == 0) { 129 /* rsync://host[:port]/module[/path] */ 130 f->remote = 1; 131 len = strlen(f->host) - 8 + 1; 132 memmove(f->host, f->host + 8, len); 133 if ((cp = strchr(f->host, '/')) == NULL) 134 errx(ERR_SYNTAX, 135 "rsync protocol requires a module name"); 136 *cp++ = '\0'; 137 f->module = cp; 138 if ((cp = strchr(f->module, '/')) != NULL) 139 *cp = '\0'; 140 if ((cp = strchr(f->host, ':')) != NULL) { 141 /* host:port --> extract port */ 142 *cp++ = '\0'; 143 opts->port = cp; 144 } 145 } else { 146 /* host:[/path] */ 147 cp = strchr(f->host, ':'); 148 assert(cp != NULL); 149 *cp++ = '\0'; 150 if (*cp == ':') { 151 /* host::module[/path] */ 152 f->remote = 1; 153 f->module = ++cp; 154 cp = strchr(f->module, '/'); 155 if (cp != NULL) 156 *cp = '\0'; 157 } 158 } 159 if ((len = strlen(f->host)) == 0) 160 errx(ERR_SYNTAX, "empty remote host"); 161 if (f->remote && strlen(f->module) == 0) 162 errx(ERR_SYNTAX, "empty remote module"); 163 } 164 165 /* Make sure we have the same "hostspec" for all files. */ 166 167 if (!f->remote) { 168 if (f->mode == FARGS_SENDER) 169 for (i = 0; i < f->sourcesz; i++) { 170 if (!fargs_is_remote(f->sources[i])) 171 continue; 172 errx(ERR_SYNTAX, 173 "remote file in list of local sources: %s", 174 f->sources[i]); 175 } 176 if (f->mode == FARGS_RECEIVER) 177 for (i = 0; i < f->sourcesz; i++) { 178 if (fargs_is_remote(f->sources[i]) && 179 !fargs_is_daemon(f->sources[i])) 180 continue; 181 if (fargs_is_daemon(f->sources[i])) 182 errx(ERR_SYNTAX, 183 "remote daemon in list of remote " 184 "sources: %s", f->sources[i]); 185 errx(ERR_SYNTAX, "local file in list of " 186 "remote sources: %s", f->sources[i]); 187 } 188 } else { 189 if (f->mode != FARGS_RECEIVER) 190 errx(ERR_SYNTAX, "sender mode for remote " 191 "daemon receivers not yet supported"); 192 for (i = 0; i < f->sourcesz; i++) { 193 if (fargs_is_daemon(f->sources[i])) 194 continue; 195 errx(ERR_SYNTAX, "non-remote daemon file " 196 "in list of remote daemon sources: " 197 "%s", f->sources[i]); 198 } 199 } 200 201 /* 202 * If we're not remote and a sender, strip our hostname. 203 * Then exit if we're a sender or a local connection. 204 */ 205 206 if (!f->remote) { 207 if (f->host == NULL) 208 return f; 209 if (f->mode == FARGS_SENDER) { 210 assert(f->host != NULL); 211 assert(len > 0); 212 j = strlen(f->sink); 213 memmove(f->sink, f->sink + len + 1, j - len); 214 return f; 215 } else if (f->mode != FARGS_RECEIVER) 216 return f; 217 } 218 219 /* 220 * Now strip the hostnames from the remote host. 221 * rsync://host/module/path -> module/path 222 * host::module/path -> module/path 223 * host:path -> path 224 * Also make sure that the remote hosts are the same. 225 */ 226 227 assert(f->host != NULL); 228 assert(len > 0); 229 230 for (i = 0; i < f->sourcesz; i++) { 231 cp = f->sources[i]; 232 j = strlen(cp); 233 if (f->remote && 234 strncasecmp(cp, "rsync://", 8) == 0) { 235 /* rsync://host[:port]/path */ 236 size_t module_offset = len; 237 cp += 8; 238 /* skip :port */ 239 if ((ccp = strchr(cp, ':')) != NULL) { 240 *ccp = '\0'; 241 module_offset += strcspn(ccp + 1, "/") + 1; 242 } 243 if (strncmp(cp, f->host, len) || 244 (cp[len] != '/' && cp[len] != '\0')) 245 errx(ERR_SYNTAX, "different remote host: %s", 246 f->sources[i]); 247 memmove(f->sources[i], 248 f->sources[i] + module_offset + 8 + 1, 249 j - module_offset - 8); 250 } else if (f->remote && strncmp(cp, "::", 2) == 0) { 251 /* ::path */ 252 memmove(f->sources[i], 253 f->sources[i] + 2, j - 1); 254 } else if (f->remote) { 255 /* host::path */ 256 if (strncmp(cp, f->host, len) || 257 (cp[len] != ':' && cp[len] != '\0')) 258 errx(ERR_SYNTAX, "different remote host: %s", 259 f->sources[i]); 260 memmove(f->sources[i], f->sources[i] + len + 2, 261 j - len - 1); 262 } else if (cp[0] == ':') { 263 /* :path */ 264 memmove(f->sources[i], f->sources[i] + 1, j); 265 } else { 266 /* host:path */ 267 if (strncmp(cp, f->host, len) || 268 (cp[len] != ':' && cp[len] != '\0')) 269 errx(ERR_SYNTAX, "different remote host: %s", 270 f->sources[i]); 271 memmove(f->sources[i], 272 f->sources[i] + len + 1, j - len); 273 } 274 } 275 276 return f; 277 } 278 279 static struct opts opts; 280 281 #define OP_ADDRESS 1000 282 #define OP_PORT 1001 283 #define OP_RSYNCPATH 1002 284 #define OP_TIMEOUT 1003 285 #define OP_EXCLUDE 1005 286 #define OP_INCLUDE 1006 287 #define OP_EXCLUDE_FROM 1007 288 #define OP_INCLUDE_FROM 1008 289 #define OP_COMP_DEST 1009 290 #define OP_COPY_DEST 1010 291 #define OP_LINK_DEST 1011 292 #define OP_MAX_SIZE 1012 293 #define OP_MIN_SIZE 1013 294 #define OP_CONTIMEOUT 1014 295 296 const struct option lopts[] = { 297 { "address", required_argument, NULL, OP_ADDRESS }, 298 { "archive", no_argument, NULL, 'a' }, 299 { "compare-dest", required_argument, NULL, OP_COMP_DEST }, 300 #if 0 301 { "copy-dest", required_argument, NULL, OP_COPY_DEST }, 302 { "link-dest", required_argument, NULL, OP_LINK_DEST }, 303 #endif 304 { "compress", no_argument, NULL, 'z' }, 305 { "contimeout", required_argument, NULL, OP_CONTIMEOUT }, 306 { "del", no_argument, &opts.del, 1 }, 307 { "delete", no_argument, &opts.del, 1 }, 308 { "devices", no_argument, &opts.devices, 1 }, 309 { "no-devices", no_argument, &opts.devices, 0 }, 310 { "dry-run", no_argument, &opts.dry_run, 1 }, 311 { "exclude", required_argument, NULL, OP_EXCLUDE }, 312 { "exclude-from", required_argument, NULL, OP_EXCLUDE_FROM }, 313 { "group", no_argument, &opts.preserve_gids, 1 }, 314 { "no-group", no_argument, &opts.preserve_gids, 0 }, 315 { "help", no_argument, NULL, 'h' }, 316 { "ignore-times", no_argument, NULL, 'I' }, 317 { "include", required_argument, NULL, OP_INCLUDE }, 318 { "include-from", required_argument, NULL, OP_INCLUDE_FROM }, 319 { "links", no_argument, &opts.preserve_links, 1 }, 320 { "max-size", required_argument, NULL, OP_MAX_SIZE }, 321 { "min-size", required_argument, NULL, OP_MIN_SIZE }, 322 { "no-links", no_argument, &opts.preserve_links, 0 }, 323 { "no-motd", no_argument, &opts.no_motd, 1 }, 324 { "numeric-ids", no_argument, &opts.numeric_ids, 1 }, 325 { "omit-dir-times", no_argument, &opts.ignore_dir_times, 1 }, 326 { "no-O", no_argument, &opts.ignore_dir_times, 0 }, 327 { "no-omit-dir-times", no_argument, &opts.ignore_dir_times, 0 }, 328 { "omit-link-times", no_argument, &opts.ignore_link_times, 1 }, 329 { "no-J", no_argument, &opts.ignore_link_times, 0 }, 330 { "no-omit-link-times", no_argument, &opts.ignore_link_times, 0 }, 331 { "owner", no_argument, &opts.preserve_uids, 1 }, 332 { "no-owner", no_argument, &opts.preserve_uids, 0 }, 333 { "perms", no_argument, &opts.preserve_perms, 1 }, 334 { "no-perms", no_argument, &opts.preserve_perms, 0 }, 335 { "port", required_argument, NULL, OP_PORT }, 336 { "recursive", no_argument, &opts.recursive, 1 }, 337 { "no-recursive", no_argument, &opts.recursive, 0 }, 338 { "rsh", required_argument, NULL, 'e' }, 339 { "rsync-path", required_argument, NULL, OP_RSYNCPATH }, 340 { "sender", no_argument, &opts.sender, 1 }, 341 { "server", no_argument, &opts.server, 1 }, 342 { "size-only", no_argument, &opts.size_only, 1 }, 343 { "specials", no_argument, &opts.specials, 1 }, 344 { "no-specials", no_argument, &opts.specials, 0 }, 345 { "timeout", required_argument, NULL, OP_TIMEOUT }, 346 { "times", no_argument, &opts.preserve_times, 1 }, 347 { "no-times", no_argument, &opts.preserve_times, 0 }, 348 { "verbose", no_argument, &verbose, 1 }, 349 { "no-verbose", no_argument, &verbose, 0 }, 350 { "version", no_argument, NULL, 'V' }, 351 { NULL, 0, NULL, 0 } 352 }; 353 354 int 355 main(int argc, char *argv[]) 356 { 357 pid_t child; 358 int fds[2], sd = -1, rc, c, st, i, lidx; 359 size_t basedir_cnt = 0; 360 struct sess sess; 361 struct fargs *fargs; 362 char **args; 363 const char *errstr; 364 365 /* Global pledge. */ 366 367 if (pledge("stdio unix rpath wpath cpath dpath inet fattr chown dns getpw proc exec unveil", 368 NULL) == -1) 369 err(ERR_IPC, "pledge"); 370 371 opts.max_size = opts.min_size = -1; 372 373 while ((c = getopt_long(argc, argv, "aDe:ghIJlnOoprtVvxz", 374 lopts, &lidx)) != -1) { 375 switch (c) { 376 case 'D': 377 opts.devices = 1; 378 opts.specials = 1; 379 break; 380 case 'a': 381 opts.recursive = 1; 382 opts.preserve_links = 1; 383 opts.preserve_perms = 1; 384 opts.preserve_times = 1; 385 opts.preserve_gids = 1; 386 opts.preserve_uids = 1; 387 opts.devices = 1; 388 opts.specials = 1; 389 break; 390 case 'e': 391 opts.ssh_prog = optarg; 392 break; 393 case 'g': 394 opts.preserve_gids = 1; 395 break; 396 case 'I': 397 opts.ignore_times = 1; 398 break; 399 case 'J': 400 opts.ignore_link_times = 1; 401 break; 402 case 'l': 403 opts.preserve_links = 1; 404 break; 405 case 'n': 406 opts.dry_run = 1; 407 break; 408 case 'O': 409 opts.ignore_dir_times = 1; 410 break; 411 case 'o': 412 opts.preserve_uids = 1; 413 break; 414 case 'p': 415 opts.preserve_perms = 1; 416 break; 417 case 'r': 418 opts.recursive = 1; 419 break; 420 case 't': 421 opts.preserve_times = 1; 422 break; 423 case 'v': 424 verbose++; 425 break; 426 case 'V': 427 fprintf(stderr, "openrsync %s (protocol version %u)\n", 428 RSYNC_VERSION, RSYNC_PROTOCOL); 429 exit(0); 430 case 'x': 431 opts.one_file_system++; 432 break; 433 case 'z': 434 fprintf(stderr, "%s: -z not supported yet\n", getprogname()); 435 break; 436 case 0: 437 /* Non-NULL flag values (e.g., --sender). */ 438 break; 439 case OP_ADDRESS: 440 opts.address = optarg; 441 break; 442 case OP_CONTIMEOUT: 443 poll_contimeout = strtonum(optarg, 0, 60*60, &errstr); 444 if (errstr != NULL) 445 errx(ERR_SYNTAX, "timeout is %s: %s", 446 errstr, optarg); 447 break; 448 case OP_PORT: 449 opts.port = optarg; 450 break; 451 case OP_RSYNCPATH: 452 opts.rsync_path = optarg; 453 break; 454 case OP_TIMEOUT: 455 poll_timeout = strtonum(optarg, 0, 60*60, &errstr); 456 if (errstr != NULL) 457 errx(ERR_SYNTAX, "timeout is %s: %s", 458 errstr, optarg); 459 break; 460 case OP_EXCLUDE: 461 if (parse_rule(optarg, RULE_EXCLUDE) == -1) 462 errx(ERR_SYNTAX, "syntax error in exclude: %s", 463 optarg); 464 break; 465 case OP_INCLUDE: 466 if (parse_rule(optarg, RULE_INCLUDE) == -1) 467 errx(ERR_SYNTAX, "syntax error in include: %s", 468 optarg); 469 break; 470 case OP_EXCLUDE_FROM: 471 parse_file(optarg, RULE_EXCLUDE); 472 break; 473 case OP_INCLUDE_FROM: 474 parse_file(optarg, RULE_INCLUDE); 475 break; 476 case OP_COMP_DEST: 477 if (opts.alt_base_mode !=0 && 478 opts.alt_base_mode != BASE_MODE_COMPARE) { 479 errx(1, "option --%s conflicts with %s", 480 lopts[lidx].name, 481 alt_base_mode(opts.alt_base_mode)); 482 } 483 opts.alt_base_mode = BASE_MODE_COMPARE; 484 #if 0 485 goto basedir; 486 case OP_COPY_DEST: 487 if (opts.alt_base_mode !=0 && 488 opts.alt_base_mode != BASE_MODE_COPY) { 489 errx(1, "option --%s conflicts with %s", 490 lopts[lidx].name, 491 alt_base_mode(opts.alt_base_mode)); 492 } 493 opts.alt_base_mode = BASE_MODE_COPY; 494 goto basedir; 495 case OP_LINK_DEST: 496 if (opts.alt_base_mode !=0 && 497 opts.alt_base_mode != BASE_MODE_LINK) { 498 errx(1, "option --%s conflicts with %s", 499 lopts[lidx].name, 500 alt_base_mode(opts.alt_base_mode)); 501 } 502 opts.alt_base_mode = BASE_MODE_LINK; 503 504 basedir: 505 #endif 506 if (basedir_cnt >= MAX_BASEDIR) 507 errx(1, "too many --%s directories specified", 508 lopts[lidx].name); 509 opts.basedir[basedir_cnt++] = optarg; 510 break; 511 case OP_MAX_SIZE: 512 if (scan_scaled(optarg, &opts.max_size) == -1) 513 err(1, "bad max-size"); 514 break; 515 case OP_MIN_SIZE: 516 if (scan_scaled(optarg, &opts.min_size) == -1) 517 err(1, "bad min-size"); 518 break; 519 case 'h': 520 default: 521 goto usage; 522 } 523 } 524 525 argc -= optind; 526 argv += optind; 527 528 /* FIXME: reference implementation rsync accepts this. */ 529 530 if (argc < 2) 531 goto usage; 532 533 if (opts.port == NULL) 534 opts.port = "rsync"; 535 536 /* by default and for --contimeout=0 disable poll_contimeout */ 537 if (poll_contimeout == 0) 538 poll_contimeout = -1; 539 else 540 poll_contimeout *= 1000; 541 542 /* by default and for --timeout=0 disable poll_timeout */ 543 if (poll_timeout == 0) 544 poll_timeout = -1; 545 else 546 poll_timeout *= 1000; 547 548 /* 549 * This is what happens when we're started with the "hidden" 550 * --server option, which is invoked for the rsync on the remote 551 * host by the parent. 552 */ 553 554 if (opts.server) 555 exit(rsync_server(&opts, (size_t)argc, argv)); 556 557 /* 558 * Now we know that we're the client on the local machine 559 * invoking rsync(1). 560 * At this point, we need to start the client and server 561 * initiation logic. 562 * The client is what we continue running on this host; the 563 * server is what we'll use to connect to the remote and 564 * invoke rsync with the --server option. 565 */ 566 567 fargs = fargs_parse(argc, argv, &opts); 568 assert(fargs != NULL); 569 570 /* 571 * If we're contacting an rsync:// daemon, then we don't need to 572 * fork, because we won't start a server ourselves. 573 * Route directly into the socket code, unless a remote shell 574 * has explicitly been specified. 575 */ 576 577 if (fargs->remote && opts.ssh_prog == NULL) { 578 assert(fargs->mode == FARGS_RECEIVER); 579 if ((rc = rsync_connect(&opts, &sd, fargs)) == 0) { 580 rc = rsync_socket(&opts, sd, fargs); 581 close(sd); 582 } 583 exit(rc); 584 } 585 586 /* Drop the dns/inet possibility. */ 587 588 if (pledge("stdio unix rpath wpath cpath dpath fattr chown getpw proc exec unveil", 589 NULL) == -1) 590 err(ERR_IPC, "pledge"); 591 592 /* Create a bidirectional socket and start our child. */ 593 594 if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0, fds) == -1) 595 err(ERR_IPC, "socketpair"); 596 597 switch ((child = fork())) { 598 case -1: 599 err(ERR_IPC, "fork"); 600 case 0: 601 close(fds[0]); 602 if (pledge("stdio exec", NULL) == -1) 603 err(ERR_IPC, "pledge"); 604 605 memset(&sess, 0, sizeof(struct sess)); 606 sess.opts = &opts; 607 608 args = fargs_cmdline(&sess, fargs, NULL); 609 610 for (i = 0; args[i] != NULL; i++) 611 LOG2("exec[%d] = %s", i, args[i]); 612 613 /* Make sure the child's stdin is from the sender. */ 614 if (dup2(fds[1], STDIN_FILENO) == -1) 615 err(ERR_IPC, "dup2"); 616 if (dup2(fds[1], STDOUT_FILENO) == -1) 617 err(ERR_IPC, "dup2"); 618 execvp(args[0], args); 619 _exit(ERR_IPC); 620 /* NOTREACHED */ 621 default: 622 close(fds[1]); 623 if (!fargs->remote) 624 rc = rsync_client(&opts, fds[0], fargs); 625 else 626 rc = rsync_socket(&opts, fds[0], fargs); 627 break; 628 } 629 630 close(fds[0]); 631 632 if (waitpid(child, &st, 0) == -1) 633 err(ERR_WAITPID, "waitpid"); 634 635 /* 636 * If we don't already have an error (rc == 0), then inherit the 637 * error code of rsync_server() if it has exited. 638 * If it hasn't exited, it overrides our return value. 639 */ 640 641 if (rc == 0) { 642 if (WIFEXITED(st)) 643 rc = WEXITSTATUS(st); 644 else if (WIFSIGNALED(st)) 645 rc = ERR_TERMIMATED; 646 else 647 rc = ERR_WAITPID; 648 } 649 650 exit(rc); 651 usage: 652 fprintf(stderr, "usage: %s" 653 " [-aDgIJlnOoprtVvx] [-e program] [--address=sourceaddr]\n" 654 "\t[--contimeout=seconds] [--compare-dest=dir] [--del] [--exclude]\n" 655 "\t[--exclude-from=file] [--include] [--include-from=file]\n" 656 "\t[--no-motd] [--numeric-ids] [--port=portnumber]\n" 657 "\t[--rsync-path=program] [--size-only] [--timeout=seconds]\n" 658 "\tsource ... directory\n", 659 getprogname()); 660 exit(ERR_SYNTAX); 661 } 662