1 /* 2 * Copyright (c)2004 The DragonFly Project. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 8 * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * 11 * Redistributions in binary form must reproduce the above copyright 12 * notice, this list of conditions and the following disclaimer in 13 * the documentation and/or other materials provided with the 14 * distribution. 15 * 16 * Neither the name of the DragonFly Project nor the names of its 17 * contributors may be used to endorse or promote products derived 18 * from this software without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 23 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 24 * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 25 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 28 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 29 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 30 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 31 * OF THE POSSIBILITY OF SUCH DAMAGE. 32 */ 33 34 /* 35 * fn_disk.c 36 * Disk functions for installer. 37 * $Id: fn_disk.c,v 1.40 2005/03/13 01:53:58 cpressey Exp $ 38 */ 39 40 #include <stdlib.h> 41 #include <string.h> 42 43 #ifdef ENABLE_NLS 44 #include <libintl.h> 45 #define _(String) gettext (String) 46 #else 47 #define _(String) (String) 48 #endif 49 50 #include "libaura/mem.h" 51 #include "libaura/fspred.h" 52 53 #include "libdfui/dfui.h" 54 #include "libdfui/system.h" 55 56 #include "libinstaller/commands.h" 57 #include "libinstaller/diskutil.h" 58 #include "libinstaller/functions.h" 59 #include "libinstaller/uiutil.h" 60 61 #include "fn.h" 62 #include "pathnames.h" 63 64 /*** DISK-RELATED FUNCTIONS ***/ 65 66 /* 67 * Ask the user which physical disk they want. 68 * Changes ss->selected_disk if successful. 69 */ 70 void 71 fn_select_disk(struct i_fn_args *a) 72 { 73 struct dfui_form *f; 74 struct dfui_action *k; 75 struct dfui_response *r; 76 struct disk *d; 77 78 f = dfui_form_create( 79 "select_disk", 80 _("Select Disk"), 81 a->short_desc, 82 "", 83 84 "p", "role", "menu", 85 "p", "special", "dfinstaller_select_disk", 86 87 NULL 88 ); 89 90 for (d = storage_disk_first(a->s); d != NULL; d = disk_next(d)) { 91 dfui_form_action_add(f, disk_get_device_name(d), 92 dfui_info_new(disk_get_desc(d), "", "")); 93 } 94 95 k = dfui_form_action_add(f, "cancel", 96 dfui_info_new(a->cancel_desc, "", "")); 97 dfui_action_property_set(k, "accelerator", "ESC"); 98 99 if (!dfui_be_present(a->c, f, &r)) 100 abort_backend(); 101 102 if (strcmp(dfui_response_get_action_id(r), "cancel") == 0) { 103 a->result = 0; 104 } else { 105 d = disk_find(a->s, dfui_response_get_action_id(r)); 106 if (d == NULL) { 107 inform(a->c, _("Internal error - response from frontend " 108 "should be a valid device name.")); 109 a->result = 0; 110 } else { 111 storage_set_selected_disk(a->s, d); 112 a->result = 1; 113 } 114 } 115 116 dfui_form_free(f); 117 dfui_response_free(r); 118 } 119 120 /* 121 * Ask the user which slice on a the selected disk they want. 122 * Changes ss->selected_slice. 123 */ 124 void 125 fn_select_slice(struct i_fn_args *a) 126 { 127 struct dfui_form *f; 128 struct dfui_action *k; 129 struct dfui_response *r; 130 struct slice *s; 131 char string[16]; 132 133 f = dfui_form_create( 134 "select_slice", 135 _("Select Primary Partition"), 136 a->short_desc, 137 "", 138 139 "p", "role", "menu", 140 "p", "special", "dfinstaller_select_slice", 141 142 NULL 143 ); 144 145 for (s = disk_slice_first(storage_get_selected_disk(a->s)); 146 s != NULL; s = slice_next(s)) { 147 snprintf(string, 16, "%d", slice_get_number(s)); 148 dfui_form_action_add(f, string, 149 dfui_info_new(slice_get_desc(s), "", "")); 150 } 151 152 k = dfui_form_action_add(f, "cancel", 153 dfui_info_new(a->cancel_desc, "", "")); 154 dfui_action_property_set(k, "accelerator", "ESC"); 155 156 if (!dfui_be_present(a->c, f, &r)) 157 abort_backend(); 158 159 if (strcmp(dfui_response_get_action_id(r), "cancel") == 0) { 160 a->result = 0; 161 } else { 162 s = slice_find(storage_get_selected_disk(a->s), 163 atoi(dfui_response_get_action_id(r))); 164 if (s == NULL) { 165 inform(a->c, _("Internal error - response from frontend " 166 "should be a valid slice number.")); 167 a->result = 0; 168 } else { 169 storage_set_selected_slice(a->s, s); 170 a->result = 1; 171 } 172 } 173 174 dfui_form_free(f); 175 dfui_response_free(r); 176 } 177 178 /* 179 * If ss->selected_disk == NULL, user will be asked for which disk. 180 * Returns 1 if disk was formatted, 0 if it wasn't. 181 * If it was, ss->selected_disk and ss->selected_slice are set to it. 182 */ 183 void 184 fn_format_disk(struct i_fn_args *a) 185 { 186 struct commands *cmds; 187 char *selected_disk_string; 188 189 if (storage_get_selected_disk(a->s) == NULL) { 190 a->short_desc = _("Select a disk to format."); 191 a->cancel_desc = _("Return to Utilities Menu"); 192 fn_select_disk(a); 193 if (!a->result || storage_get_selected_disk(a->s) == NULL) { 194 a->result = 0; 195 return; 196 } 197 } 198 199 if (confirm_dangerous_action(a->c, 200 _("WARNING! ALL data in ALL partitions on the disk\n\n" 201 "%s\n\nwill be IRREVOCABLY ERASED!\n\nAre you ABSOLUTELY " 202 "SURE you wish to take this action? This is your " 203 "LAST CHANCE to cancel!"), disk_get_desc(storage_get_selected_disk(a->s)))) { 204 cmds = commands_new(); 205 206 command_add(cmds, "%s%s -BI %s", 207 a->os_root, cmd_name(a, "FDISK"), 208 disk_get_device_name(storage_get_selected_disk(a->s))); 209 210 if (!commands_execute(a, cmds)) { 211 inform(a->c, _("The disk\n\n%s\n\nwas " 212 "not correctly formatted, and may " 213 "now be in an inconsistent state. " 214 "We recommend re-formatting it " 215 "before attempting to install " 216 "%s on it."), 217 disk_get_desc(storage_get_selected_disk(a->s)), 218 OPERATING_SYSTEM_NAME); 219 commands_free(cmds); 220 a->result = 0; 221 return; 222 } 223 commands_free(cmds); 224 225 /* 226 * Since one of the disks has now changed, we must 227 * refresh our view of them and re-select the disk 228 * since the selected_disk pointer will be invalidated. 229 */ 230 selected_disk_string = aura_strdup( 231 disk_get_device_name(storage_get_selected_disk(a->s))); 232 if (!survey_storage(a)) { 233 inform(a->c, _("Errors occurred while probing " 234 "the system for its storage capabilities.")); 235 } 236 storage_set_selected_disk(a->s, disk_find(a->s, selected_disk_string)); 237 free(selected_disk_string); 238 239 /* 240 * Note that we formatted this disk and that we want 241 * to use the first (and only) slice of it. 242 */ 243 disk_set_formatted(storage_get_selected_disk(a->s), 1); 244 storage_set_selected_slice(a->s, disk_slice_first(storage_get_selected_disk(a->s))); 245 246 if (!format_slice(a)) { 247 inform(a->c, _("The sole primary partition of " 248 "the disk\n\n%s\n\nwas " 249 "not correctly formatted, and may " 250 "now be in an inconsistent state. " 251 "We recommend re-formatting the " 252 "disk before attempting to install " 253 "%s on it."), 254 disk_get_desc(storage_get_selected_disk(a->s)), 255 OPERATING_SYSTEM_NAME); 256 a->result = 0; 257 return; 258 } 259 260 inform(a->c, _("The disk\n\n%s\n\nwas formatted."), 261 disk_get_desc(storage_get_selected_disk(a->s))); 262 a->result = 1; 263 } else { 264 inform(a->c, _("Action cancelled - no disks were formatted.")); 265 a->result = 0; 266 } 267 } 268 269 /* 270 * Wipes the start of the selected disk. 271 */ 272 void 273 fn_wipe_start_of_disk(struct i_fn_args *a) 274 { 275 struct commands *cmds; 276 277 a->short_desc = _("If you are having problems formatting a disk, " 278 "it may be because of junk that has accumulated " 279 "in the boot block and the partition table. " 280 "A cure for this is to wipe out everything on " 281 "the first few sectors of the disk. However, this " 282 "is a rather drastic action to take, so it is not " 283 "recommended unless you are otherwise " 284 "encountering problems."); 285 a->cancel_desc = _("Return to Utilities Menu"); 286 fn_select_disk(a); 287 if (!a->result) 288 return; 289 290 /* XXX check to make sure no slices on this disk are mounted first? */ 291 if (storage_get_selected_disk(a->s) != NULL && confirm_dangerous_action(a->c, 292 _("WARNING! ALL data in ALL partitions on the disk\n\n" 293 "%s\n\nwill be IRREVOCABLY ERASED!\n\nAre you ABSOLUTELY " 294 "SURE you wish to take this action? This is your " 295 "LAST CHANCE to cancel!"), disk_get_desc(storage_get_selected_disk(a->s)))) { 296 cmds = commands_new(); 297 command_add(cmds, 298 "%s%s if=/dev/zero of=/dev/%s bs=32k count=16", 299 a->os_root, cmd_name(a, "DD"), 300 disk_get_device_name(storage_get_selected_disk(a->s))); 301 if (commands_execute(a, cmds)) { 302 inform(a->c, _("Start of disk was successfully wiped.")); 303 } else { 304 inform(a->c, _("Some errors occurred. " 305 "Start of disk was not successfully wiped.")); 306 } 307 commands_free(cmds); 308 } 309 } 310 311 /* 312 * Wipes the start of the selected slice. 313 */ 314 void 315 fn_wipe_start_of_slice(struct i_fn_args *a) 316 { 317 struct commands *cmds; 318 319 a->short_desc = 320 _("If you are having problems formatting a primary partition, " 321 "it may be because of junk that has accumulated in the " 322 "partition's `disklabel'. A cure for this is to wipe out " 323 "everything on the first few sectors of the primary partition. " 324 "However, this is a rather drastic action to take, so it is not " 325 "recommended unless you are otherwise encountering problems."); 326 a->cancel_desc = _("Return to Utilities Menu"); 327 fn_select_slice(a); 328 if (!a->result) 329 return; 330 331 if (confirm_dangerous_action(a->c, 332 _("WARNING! ALL data in primary partition #%d,\n\n%s\n\non the " 333 "disk\n\n%s\n\n will be IRREVOCABLY ERASED!\n\nAre you " 334 "ABSOLUTELY SURE you wish to take this action? This is " 335 "your LAST CHANCE to cancel!"), 336 slice_get_number(storage_get_selected_slice(a->s)), 337 slice_get_desc(storage_get_selected_slice(a->s)), 338 disk_get_desc(storage_get_selected_disk(a->s)))) { 339 /* XXX check to make sure this slice is not mounted first */ 340 cmds = commands_new(); 341 command_add(cmds, "%s%s if=/dev/zero of=/dev/%s bs=32k count=16", 342 a->os_root, cmd_name(a, "DD"), 343 slice_get_device_name(storage_get_selected_slice(a->s))); 344 if (commands_execute(a, cmds)) { 345 inform(a->c, _("Start of primary partition was successfully wiped.")); 346 } else { 347 inform(a->c, _("Some errors occurred. " 348 "Start of primary partition was not successfully wiped.")); 349 } 350 commands_free(cmds); 351 } 352 } 353 354 static void 355 ask_to_wipe_boot_sector(struct i_fn_args *a, struct commands *fcmds) 356 { 357 struct commands *cmds; 358 struct command *cmd; 359 char *disk; 360 361 for (cmd = command_get_first(fcmds); cmd != NULL; 362 cmd = command_get_next(cmd)) { 363 disk = command_get_tag(cmd); 364 if (disk != NULL && 365 command_get_result(cmd) > 0 && 366 command_get_result(cmd) < 256) { 367 switch (dfui_be_present_dialog(a->c, 368 _("Bootblock Install Failed"), 369 _("Re-Initialize Bootblock|Cancel"), 370 _("Warning: bootblocks were not successfully " 371 "installed on the disk `%s'. This may be " 372 "because the disk is new and not yet " 373 "formatted. If this is the case, it might " 374 "help to re-initialize the boot sector, " 375 "then try installing the bootblock again. " 376 "Note that this should not affect the " 377 "partition table of the disk."), 378 disk)) { 379 case 1: 380 cmds = commands_new(); 381 command_add(cmds, 382 "%s%s | %s%s -B /dev/%s", 383 a->os_root, cmd_name(a, "YES"), 384 a->os_root, cmd_name(a, "FDISK"), 385 disk); 386 if (commands_execute(a, cmds)) { 387 inform(a->c, _("Boot sector successfully initialized.")); 388 } else { 389 inform(a->c, _("Some errors occurred. " 390 "Boot sector was not successfully initialized.")); 391 } 392 commands_free(cmds); 393 break; 394 default: 395 break; 396 } 397 } 398 } 399 } 400 401 void 402 fn_install_bootblocks(struct i_fn_args *a, const char *device) 403 { 404 struct dfui_form *f; 405 struct dfui_response *r; 406 struct dfui_dataset *ds; 407 struct disk *d; 408 struct commands *cmds; 409 struct command *cmd; 410 char disk[64], boot0cfg[32], packet[32]; 411 char msg_buf[1][1024]; 412 413 snprintf(msg_buf[0], sizeof(msg_buf[0]), 414 "'Packet Mode' refers to using newer BIOS calls to boot " 415 "from a partition of the disk. It is generally not " 416 "required unless:\n\n" 417 "- your BIOS does not support legacy mode; or\n" 418 "- your %s primary partition resides on a " 419 "cylinder of the disk beyond cylinder 1024; or\n" 420 "- you just can't get it to boot without it.", 421 OPERATING_SYSTEM_NAME); 422 423 f = dfui_form_create( 424 "install_bootstrap", 425 _("Install Bootblock(s)"), 426 a->short_desc, 427 428 msg_buf[0], 429 430 "p", "special", "dfinstaller_install_bootstrap", 431 432 "f", "disk", _("Disk Drive"), 433 _("The disk on which you wish to install a bootblock"), "", 434 "p", "editable", "false", 435 "f", "boot0cfg", _("Install Bootblock?"), 436 _("Install a bootblock on this disk"), "", 437 "p", "control", "checkbox", 438 "f", "packet", _("Packet Mode?"), 439 _("Select this to use 'packet mode' to boot the disk"), "", 440 "p", "control", "checkbox", 441 442 "a", "ok", _("Accept and Install Bootblocks"), "", "", 443 "a", "cancel", a->cancel_desc, "", "", 444 "p", "accelerator", "ESC", 445 446 NULL 447 ); 448 449 dfui_form_set_multiple(f, 1); 450 451 if (device != NULL) { 452 ds = dfui_dataset_new(); 453 dfui_dataset_celldata_add(ds, "disk", device); 454 dfui_dataset_celldata_add(ds, "boot0cfg", "Y"); 455 dfui_dataset_celldata_add(ds, "packet", "Y"); 456 dfui_form_dataset_add(f, ds); 457 } else { 458 for (d = storage_disk_first(a->s); d != NULL; d = disk_next(d)) { 459 ds = dfui_dataset_new(); 460 dfui_dataset_celldata_add(ds, "disk", 461 disk_get_device_name(d)); 462 dfui_dataset_celldata_add(ds, "boot0cfg", "Y"); 463 dfui_dataset_celldata_add(ds, "packet", "Y"); 464 dfui_form_dataset_add(f, ds); 465 } 466 } 467 468 if (!dfui_be_present(a->c, f, &r)) 469 abort_backend(); 470 471 a->result = 0; 472 if (strcmp(dfui_response_get_action_id(r), "ok") == 0) { 473 cmds = commands_new(); 474 475 for (ds = dfui_response_dataset_get_first(r); ds != NULL; 476 ds = dfui_dataset_get_next(ds)) { 477 strlcpy(disk, dfui_dataset_get_value(ds, "disk"), 64); 478 strlcpy(boot0cfg, dfui_dataset_get_value(ds, "boot0cfg"), 32); 479 strlcpy(packet, dfui_dataset_get_value(ds, "packet"), 32); 480 481 if (strcasecmp(boot0cfg, "Y") == 0) { 482 cmd = command_add(cmds, "%s%s -B -o %spacket %s", 483 a->os_root, cmd_name(a, "BOOT0CFG"), 484 strcasecmp(packet, "Y") == 0 ? "" : "no", 485 disk); 486 command_set_failure_mode(cmd, COMMAND_FAILURE_WARN); 487 command_set_tag(cmd, "%s", disk); 488 cmd = command_add(cmds, "%s%s -v %s", 489 a->os_root, cmd_name(a, "BOOT0CFG"), 490 disk); 491 command_set_failure_mode(cmd, COMMAND_FAILURE_WARN); 492 command_set_tag(cmd, "%s", disk); 493 } 494 } 495 496 if (!commands_execute(a, cmds)) { 497 ask_to_wipe_boot_sector(a, cmds); 498 } else { 499 inform(a->c, _("Bootblocks were successfully installed!")); 500 a->result = 1; 501 } 502 commands_free(cmds); 503 } 504 505 dfui_form_free(f); 506 dfui_response_free(r); 507 } 508 509 void 510 fn_format_msdos_floppy(struct i_fn_args *a) 511 { 512 struct commands *cmds; 513 514 switch (dfui_be_present_dialog(a->c, _("Format MSDOS Floppy"), 515 _("Format Floppy|Return to Utilities Menu"), 516 _("Please insert the floppy to be formatted " 517 "in unit 0 (``drive A:'')."))) { 518 case 1: 519 cmds = commands_new(); 520 command_add(cmds, "%s%s -y -f 1440 /dev/fd0", 521 a->os_root, cmd_name(a, "FDFORMAT")); 522 command_add(cmds, "%s%s -f 1440 fd0", 523 a->os_root, cmd_name(a, "NEWFS_MSDOS")); 524 if (commands_execute(a, cmds)) 525 inform(a->c, _("Floppy successfully formatted!")); 526 else 527 inform(a->c, _("Floppy was not successfully formatted.")); 528 break; 529 case 2: 530 return; 531 default: 532 abort_backend(); 533 } 534 } 535 536 void 537 fn_create_cdboot_floppy(struct i_fn_args *a) 538 { 539 struct commands *cmds; 540 char msg_buf[1][1024]; 541 542 snprintf(msg_buf[0], sizeof(msg_buf[0]), 543 "%s cannot be installed from a floppy; " 544 "it must be installed from a booted CD-ROM. " 545 "However, many older systems do not support booting " 546 "from a CD-ROM. For these systems, a boot disk can be " 547 "created. This boot disk contains the Smart Boot " 548 "Manager program, which can boot a CD-ROM even " 549 "on systems with BIOSes which do not support booting " 550 "from the CD-ROM.\n\n" 551 "Smart Boot Manager is not a part of %s; " 552 "the Smart Boot Manager project can be found here:\n\n" 553 "http://btmgr.sourceforge.net/\n\n" 554 "To create a CDBoot floppy, insert a blank floppy " 555 "in unit 0 (``drive A:'') before proceeding." 556 "", 557 OPERATING_SYSTEM_NAME, OPERATING_SYSTEM_NAME); 558 559 switch (dfui_be_present_dialog(a->c, _("Create CDBoot Floppy"), 560 _("Create CDBoot Floppy|Return to Utilities Menu"), 561 "%s", msg_buf[0])) { 562 case 1: 563 cmds = commands_new(); 564 command_add(cmds, "%s%s -c %sboot/cdboot.flp.bz2 | " 565 "%s%s of=/dev/fd0 bs=32k", 566 a->os_root, cmd_name(a, "BUNZIP2"), 567 a->os_root, 568 a->os_root, cmd_name(a, "DD")); 569 if (commands_execute(a, cmds)) 570 inform(a->c, _("CDBoot floppy successfully created!")); 571 else 572 inform(a->c, _("CDBoot floppy was not successfully created.")); 573 break; 574 case 2: 575 return; 576 default: 577 abort_backend(); 578 } 579 } 580 581 /**** NON-fn_ FUNCTIONS ***/ 582 583 int 584 format_slice(struct i_fn_args *a) 585 { 586 struct commands *cmds; 587 struct command *cmd; 588 int result; 589 int cyl, hd, sec; 590 591 cmds = commands_new(); 592 593 /* 594 * The information in a->s NEEDS to be accurate here! 595 * Presumably we just did a survey_storage() recently. 596 * XXX should we do another one here anyway just to be paranoid? 597 */ 598 599 /* 600 * Make sure the survey did get disk info correctly or fail 601 */ 602 if ((storage_get_selected_disk(a->s) == NULL) || 603 (storage_get_selected_slice(a->s) == NULL)) 604 return 0; 605 606 /* 607 * Set the slice's sysid to 165. 608 */ 609 disk_get_geometry(storage_get_selected_disk(a->s), &cyl, &hd, &sec); 610 command_add(cmds, "%s%s 'g c%d h%d s%d' >%snew.fdisk", 611 a->os_root, cmd_name(a, "ECHO"), 612 cyl, hd, sec, 613 a->tmp); 614 command_add(cmds, "%s%s 'p %d %d %lu %lu' >>%snew.fdisk", 615 a->os_root, cmd_name(a, "ECHO"), 616 slice_get_number(storage_get_selected_slice(a->s)), 617 165, 618 slice_get_start(storage_get_selected_slice(a->s)), 619 slice_get_size(storage_get_selected_slice(a->s)), 620 a->tmp); 621 if (slice_get_flags(storage_get_selected_slice(a->s)) & 0x80) { 622 command_add(cmds, "%s%s 'a %d' >>%snew.fdisk", 623 a->os_root, cmd_name(a, "ECHO"), 624 slice_get_number(storage_get_selected_slice(a->s)), 625 a->tmp); 626 } 627 628 command_add(cmds, "%s%s %snew.fdisk", 629 a->os_root, cmd_name(a, "CAT"), a->tmp); 630 temp_file_add(a, "new.fdisk"); 631 632 /* 633 * Execute the fdisk script. 634 */ 635 cmd = command_add(cmds, "%s%s -v -f %snew.fdisk %s", 636 a->os_root, cmd_name(a, "FDISK"), a->tmp, 637 disk_get_device_name(storage_get_selected_disk(a->s))); 638 if (slice_get_size(storage_get_selected_slice(a->s)) == 0xFFFFFFFFU) 639 command_set_failure_mode(cmd, COMMAND_FAILURE_IGNORE); 640 641 /* 642 * If there is an old 'virgin' disklabel hanging around 643 * in the temp dir, get rid of it. This won't happen 644 * from a real CD, but might happen with '-o' installs. 645 */ 646 command_add(cmds, "%s%s -f %sinstall.disklabel.%s", 647 a->os_root, cmd_name(a, "RM"), 648 a->tmp, 649 slice_get_device_name(storage_get_selected_slice(a->s))); 650 651 result = commands_execute(a, cmds); 652 653 commands_free(cmds); 654 655 return(result); 656 } 657