1 /**
   2  * ntfsls - Part of the Linux-NTFS project.
   3  *
   4  * Copyright (c) 2003 Lode Leroy
   5  * Copyright (c) 2003-2005 Anton Altaparmakov
   6  * Copyright (c) 2003 Richard Russon
   7  * Copyright (c) 2004 Carmelo Kintana
   8  * Copyright (c) 2004 Giang Nguyen
   9  *
  10  * This utility will list a directory's files.
  11  *
  12  * This program is free software; you can redistribute it and/or modify
  13  * it under the terms of the GNU General Public License as published by
  14  * the Free Software Foundation; either version 2 of the License, or
  15  * (at your option) any later version.
  16  *
  17  * This program is distributed in the hope that it will be useful,
  18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
  19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  20  * GNU General Public License for more details.
  21  *
  22  * You should have received a copy of the GNU General Public License
  23  * along with this program (in the main directory of the Linux-NTFS
  24  * distribution in the file COPYING); if not, write to the Free Software
  25  * Foundation,Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  26  */
  27 #include "config.h"
  28 
  29 #ifdef HAVE_STDIO_H
  30 #include <stdio.h>
  31 #endif
  32 #ifdef HAVE_STDLIB_H
  33 #include <stdlib.h>
  34 #endif
  35 #ifdef HAVE_TIME_H
  36 #include <time.h>
  37 #endif
  38 #ifdef HAVE_GETOPT_H
  39 #include <getopt.h>
  40 #endif
  41 #ifdef HAVE_STRING_H
  42 #include <string.h>
  43 #endif
  44 
  45 #include "compat.h"
  46 #include "types.h"
  47 #include "mft.h"
  48 #include "attrib.h"
  49 #include "layout.h"
  50 #include "inode.h"
  51 #include "utils.h"
  52 #include "dir.h"
  53 #include "list.h"
  54 #include "ntfstime.h"
  55 #include "version.h"
  56 #include "logging.h"
  57 
  58 static const char *EXEC_NAME = "ntfsls";
  59 
  60 /**
  61  * To hold sub-directory information for recursive listing.
  62  * @depth:     the level of this dir relative to opts.path
  63  */
  64 struct dir {
  65         struct list_head list;
  66         ntfs_inode *ni;
  67         char name[MAX_PATH];
  68         int depth;
  69 };
  70 
  71 /**
  72  * path_component - to store path component strings
  73  *
  74  * @name: string pointer
  75  *
  76  * NOTE: @name is not directly allocated memory. It simply points to the
  77  * character array name in struct dir.
  78  */
  79 struct path_component {
  80         struct list_head list;
  81         const char *name;
  82 };
  83 
  84 /* The list of sub-dirs is like a "horizontal" tree. The root of
  85  * the tree is opts.path, but it is not part of the list because
  86  * that's not necessary. The rules of the list are (in order of
  87  * precedence):
  88  * 1. directories immediately follow their parent.
  89  * 2. siblings are next to one another.
  90  *
  91  * For example, if:
  92  *   1. opts.path is /
  93  *   2. /    has 2 sub-dirs: dir1 and dir2
  94  *   3. dir1 has 2 sub-dirs: dir11 and dir12
  95  *   4. dir2 has 0 sub-dirs
  96  * then the list will be:
  97  * dummy head -> dir1 -> dir11 -> dir12 -> dir2
  98  *
  99  * dir_list_insert_pos keeps track of where to insert a sub-dir
 100  * into the list.
 101  */
 102 static struct list_head *dir_list_insert_pos = NULL;
 103 
 104 /* The global depth relative to opts.path.
 105  * ie: opts.path has depth 0, a sub-dir of opts.path has depth 1
 106  */
 107 static int depth = 0;
 108 
 109 static struct options {
 110         char *device;   /* Device/File to work with */
 111         int quiet;      /* Less output */
 112         int verbose;    /* Extra output */
 113         int force;      /* Override common sense */
 114         int all;
 115         int system;
 116         int dos;
 117         int lng;
 118         int inode;
 119         int classify;
 120         int recursive;
 121         const char *path;
 122 } opts;
 123 
 124 typedef struct {
 125         ntfs_volume *vol;
 126 } ntfsls_dirent;
 127 
 128 static int list_dir_entry(ntfsls_dirent * dirent, const ntfschar * name,
 129                           const int name_len, const int name_type,
 130                           const s64 pos, const MFT_REF mref,
 131                           const unsigned dt_type);
 132 
 133 /**
 134  * version - Print version information about the program
 135  *
 136  * Print a copyright statement and a brief description of the program.
 137  *
 138  * Return:  none
 139  */
 140 static void version(void)
 141 {
 142         printf("\n%s v%s (libntfs %s) - Display information about an NTFS "
 143                         "Volume.\n\n", EXEC_NAME, VERSION,
 144                         ntfs_libntfs_version());
 145         printf("Copyright (c) 2003 Lode Leroy\n");
 146         printf("Copyright (c) 2003-2005 Anton Altaparmakov\n");
 147         printf("Copyright (c) 2003 Richard Russon\n");
 148         printf("Copyright (c) 2004 Carmelo Kintana\n");
 149         printf("Copyright (c) 2004 Giang Nguyen\n");
 150         printf("\n%s\n%s%s\n", ntfs_gpl, ntfs_bugs, ntfs_home);
 151 }
 152 
 153 /**
 154  * usage - Print a list of the parameters to the program
 155  *
 156  * Print a list of the parameters and options for the program.
 157  *
 158  * Return:  none
 159  */
 160 static void usage(void)
 161 {
 162         printf("\nUsage: %s [options] device\n"
 163                 "\n"
 164                 "    -a, --all            Display all files\n"
 165                 "    -F, --classify       Display classification\n"
 166                 "    -f, --force          Use less caution\n"
 167                 "    -h, --help           Display this help\n"
 168                 "    -i, --inode          Display inode numbers\n"
 169                 "    -l, --long           Display long info\n"
 170                 "    -p, --path PATH      Directory whose contents to list\n"
 171                 "    -q, --quiet          Less output\n"
 172                 "    -R, --recursive      Recursively list subdirectories\n"
 173                 "    -s, --system         Display system files\n"
 174                 "    -V, --version        Display version information\n"
 175                 "    -v, --verbose        More output\n"
 176                 "    -x, --dos            Use short (DOS 8.3) names\n"
 177                 "\n",
 178                 EXEC_NAME);
 179 
 180         printf("NOTE: If neither -a nor -s is specified, the program defaults to -a.\n\n");
 181 
 182         printf("%s%s\n", ntfs_bugs, ntfs_home);
 183 }
 184 
 185 /**
 186  * parse_options - Read and validate the programs command line
 187  *
 188  * Read the command line, verify the syntax and parse the options.
 189  * This function is very long, but quite simple.
 190  *
 191  * Return:  1 Success
 192  *          0 Error, one or more problems
 193  */
 194 static int parse_options(int argc, char *argv[])
 195 {
 196         static const char *sopt = "-aFfh?ilp:qRsVvx";
 197         static const struct option lopt[] = {
 198                 { "all",         no_argument,           NULL, 'a' },
 199                 { "classify",    no_argument,           NULL, 'F' },
 200                 { "force",       no_argument,           NULL, 'f' },
 201                 { "help",        no_argument,           NULL, 'h' },
 202                 { "inode",       no_argument,           NULL, 'i' },
 203                 { "long",        no_argument,           NULL, 'l' },
 204                 { "path",        required_argument,     NULL, 'p' },
 205                 { "recursive",   no_argument,           NULL, 'R' },
 206                 { "quiet",       no_argument,           NULL, 'q' },
 207                 { "system",      no_argument,           NULL, 's' },
 208                 { "version",     no_argument,           NULL, 'V' },
 209                 { "verbose",     no_argument,           NULL, 'v' },
 210                 { "dos",         no_argument,           NULL, 'x' },
 211                 { NULL, 0, NULL, 0 },
 212         };
 213 
 214         int c = -1;
 215         int err  = 0;
 216         int ver  = 0;
 217         int help = 0;
 218         int levels = 0;
 219 
 220         opterr = 0; /* We'll handle the errors, thank you. */
 221 
 222         memset(&opts, 0, sizeof(opts));
 223         opts.device = NULL;
 224         opts.path = "/";
 225 
 226         while ((c = getopt_long(argc, argv, sopt, lopt, NULL)) != -1) {
 227                 switch (c) {
 228                 case 1:
 229                         if (!opts.device)
 230                                 opts.device = optarg;
 231                         else
 232                                 err++;
 233                         break;
 234                 case 'p':
 235                         opts.path = optarg;
 236                         break;
 237                 case 'f':
 238                         opts.force++;
 239                         break;
 240                 case 'h':
 241                 case '?':
 242                         if (strncmp (argv[optind-1], "--log-", 6) == 0) {
 243                                 if (!ntfs_log_parse_option (argv[optind-1]))
 244                                         err++;
 245                                 break;
 246                         }
 247                         help++;
 248                         break;
 249                 case 'q':
 250                         opts.quiet++;
 251                         ntfs_log_clear_levels(NTFS_LOG_LEVEL_QUIET);
 252                         break;
 253                 case 'v':
 254                         opts.verbose++;
 255                         ntfs_log_set_levels(NTFS_LOG_LEVEL_VERBOSE);
 256                         break;
 257                 case 'V':
 258                         ver++;
 259                         break;
 260                 case 'x':
 261                         opts.dos = 1;
 262                         break;
 263                 case 'l':
 264                         opts.lng++;
 265                         break;
 266                 case 'i':
 267                         opts.inode++;
 268                         break;
 269                 case 'F':
 270                         opts.classify++;
 271                         break;
 272                 case 'a':
 273                         opts.all++;
 274                         break;
 275                 case 's':
 276                         opts.system++;
 277                         break;
 278                 case 'R':
 279                         opts.recursive++;
 280                         break;
 281                 default:
 282                         ntfs_log_error("Unknown option '%s'.\n", argv[optind - 1]);
 283                         err++;
 284                         break;
 285                 }
 286         }
 287 
 288         /* Make sure we're in sync with the log levels */
 289         levels = ntfs_log_get_levels();
 290         if (levels & NTFS_LOG_LEVEL_VERBOSE)
 291                 opts.verbose++;
 292         if (!(levels & NTFS_LOG_LEVEL_QUIET))
 293                 opts.quiet++;
 294 
 295         /* defaults to -a if -s is not specified */
 296         if (!opts.system)
 297                 opts.all++;
 298 
 299         if (help || ver)
 300                 opts.quiet = 0;
 301         else {
 302                 if (opts.device == NULL) {
 303                         if (argc > 1)
 304                                 ntfs_log_error("You must specify exactly one "
 305                                                 "device.\n");
 306                         err++;
 307                 }
 308 
 309                 if (opts.quiet && opts.verbose) {
 310                         ntfs_log_error("You may not use --quiet and --verbose at the "
 311                                         "same time.\n");
 312                         err++;
 313                 }
 314         }
 315 
 316         if (ver)
 317                 version();
 318         if (help || err)
 319                 usage();
 320 
 321         return (!err && !help && !ver);
 322 }
 323 
 324 /**
 325  * free_dir - free one dir
 326  * @tofree:   the dir to free
 327  *
 328  * Close the inode and then free the dir
 329  */
 330 static void free_dir(struct dir *tofree)
 331 {
 332         if (tofree) {
 333                 if (tofree->ni) {
 334                         ntfs_inode_close(tofree->ni);
 335                         tofree->ni = NULL;
 336                 }
 337                 free(tofree);
 338         }
 339 }
 340 
 341 /**
 342  * free_dirs - walk the list of dir's and free each of them
 343  * @dir_list:    the list_head of any entry in the list
 344  *
 345  * Iterate over @dir_list, calling free_dir on each entry
 346  */
 347 static void free_dirs(struct list_head *dir_list)
 348 {
 349         struct dir *tofree = NULL;
 350         struct list_head *walker = NULL;
 351 
 352         if (dir_list) {
 353                 list_for_each(walker, dir_list) {
 354                         free_dir(tofree);
 355                         tofree = list_entry(walker, struct dir, list);
 356                 }
 357 
 358                 free_dir(tofree);
 359         }
 360 }
 361 
 362 /**
 363  * readdir_recursive - list a directory and sub-directories encountered
 364  * @ni:         ntfs inode of the directory to list
 365  * @pos:        current position in directory
 366  * @dirent:     context for filldir callback supplied by the caller
 367  *
 368  * For each directory, print its path relative to opts.path. List a directory,
 369  * then list each of its sub-directories.
 370  *
 371  * Returns 0 on success or -1 on error.
 372  *
 373  * NOTE: Assumes recursive option. Currently no limit on the depths of
 374  * recursion.
 375  */
 376 static int readdir_recursive(ntfs_inode * ni, s64 * pos, ntfsls_dirent * dirent)
 377 {
 378         /* list of dirs to "ls" recursively */
 379         static struct dir dirs = {
 380                 .list = LIST_HEAD_INIT(dirs.list),
 381                 .ni = NULL,
 382                 .name = {0},
 383                 .depth = 0
 384         };
 385 
 386         static struct path_component paths = {
 387                 .list = LIST_HEAD_INIT(paths.list),
 388                 .name = NULL
 389         };
 390 
 391         static struct path_component base_comp;
 392 
 393         struct dir *subdir = NULL;
 394         struct dir *tofree = NULL;
 395         struct path_component comp;
 396         struct path_component *tempcomp = NULL;
 397         struct list_head *dir_walker = NULL;
 398         struct list_head *comp_walker = NULL;
 399         s64 pos2 = 0;
 400         int ni_depth = depth;
 401         int result = 0;
 402 
 403         if (list_empty(&dirs.list)) {
 404                 base_comp.name = opts.path;
 405                 list_add(&base_comp.list, &paths.list);
 406                 dir_list_insert_pos = &dirs.list;
 407                 printf("%s:\n", opts.path);
 408         }
 409 
 410         depth++;
 411 
 412         result = ntfs_readdir(ni, pos, dirent, (ntfs_filldir_t) list_dir_entry);
 413 
 414         if (result == 0) {
 415                 list_add_tail(&comp.list, &paths.list);
 416 
 417                 /* for each of ni's sub-dirs: list in this iteration, then
 418                    free at the top of the next iteration or outside of loop */
 419                 list_for_each(dir_walker, &dirs.list) {
 420                         if (tofree) {
 421                                 free_dir(tofree);
 422                                 tofree = NULL;
 423                         }
 424                         subdir = list_entry(dir_walker, struct dir, list);
 425 
 426                         /* subdir is not a subdir of ni */
 427                         if (subdir->depth != ni_depth + 1)
 428                                 break;
 429 
 430                         pos2 = 0;
 431                         dir_list_insert_pos = &dirs.list;
 432                         if (!subdir->ni) {
 433                                 subdir->ni =
 434                                     ntfs_pathname_to_inode(ni->vol, ni,
 435                                                             subdir->name);
 436 
 437                                 if (!subdir->ni) {
 438                                         ntfs_log_error
 439                                             ("ntfsls::readdir_recursive(): cannot get inode from pathname.\n");
 440                                         result = -1;
 441                                         break;
 442                                 }
 443                         }
 444                         puts("");
 445 
 446                         comp.name = subdir->name;
 447 
 448                         /* print relative path header */
 449                         list_for_each(comp_walker, &paths.list) {
 450                                 tempcomp =
 451                                     list_entry(comp_walker,
 452                                                struct path_component, list);
 453                                 printf("%s", tempcomp->name);
 454                                 if (tempcomp != &comp
 455                                     && *tempcomp->name != PATH_SEP
 456                                     && (!opts.classify
 457                                         || tempcomp == &base_comp))
 458                                         putchar(PATH_SEP);
 459                         }
 460                         puts(":");
 461 
 462                         result = readdir_recursive(subdir->ni, &pos2, dirent);
 463 
 464                         if (result)
 465                                 break;
 466 
 467                         tofree = subdir;
 468                         list_del(dir_walker);
 469                 }
 470 
 471                 list_del(&comp.list);
 472         }
 473 
 474         if (tofree)
 475                 free_dir(tofree);
 476 
 477         /* if at the outer-most readdir_recursive, then clean up */
 478         if (ni_depth == 0) {
 479                 free_dirs(&dirs.list);
 480         }
 481 
 482         depth--;
 483 
 484         return result;
 485 }
 486 
 487 /**
 488  * list_dir_entry
 489  *
 490  * FIXME: Should we print errors as we go along? (AIA)
 491  */
 492 static int list_dir_entry(ntfsls_dirent * dirent, const ntfschar * name,
 493                           const int name_len, const int name_type,
 494                           const s64 pos __attribute__((unused)),
 495                           const MFT_REF mref, const unsigned dt_type)
 496 {
 497         char *filename = NULL;
 498         int result = 0;
 499 
 500         struct dir *dir = NULL;
 501 
 502         filename = calloc(1, MAX_PATH);
 503         if (!filename)
 504                 return -1;
 505 
 506         if (ntfs_ucstombs(name, name_len, &filename, MAX_PATH) < 0) {
 507                 ntfs_log_error("Cannot represent filename in current locale.\n");
 508                 goto free;
 509         }
 510 
 511         result = 0;                                     // These are successful
 512         if ((MREF(mref) < FILE_first_user) && (!opts.system))
 513                 goto free;
 514         if (name_type == FILE_NAME_POSIX && !opts.all)
 515                 goto free;
 516         if (((name_type & FILE_NAME_WIN32_AND_DOS) == FILE_NAME_WIN32) &&
 517                         opts.dos)
 518                 goto free;
 519         if (((name_type & FILE_NAME_WIN32_AND_DOS) == FILE_NAME_DOS) &&
 520                         !opts.dos)
 521                 goto free;
 522         if (dt_type == NTFS_DT_DIR && opts.classify)
 523                 sprintf(filename + strlen(filename), "/");
 524 
 525         if (dt_type == NTFS_DT_DIR && opts.recursive
 526             && strcmp(filename, ".") && strcmp(filename, "./")
 527             && strcmp(filename, "..") && strcmp(filename, "../"))
 528         {
 529                 dir = (struct dir *)calloc(1, sizeof(struct dir));
 530 
 531                 if (!dir) {
 532                         ntfs_log_error("Failed to allocate for subdir.\n");
 533                         result = -1;
 534                         goto free;
 535                 }
 536 
 537                 strcpy(dir->name, filename);
 538                 dir->ni = NULL;
 539                 dir->depth = depth;
 540         }
 541 
 542         if (!opts.lng) {
 543                 if (!opts.inode)
 544                         printf("%s\n", filename);
 545                 else
 546                         printf("%7llu %s\n", (unsigned long long)MREF(mref),
 547                                         filename);
 548                 result = 0;
 549         } else {
 550                 s64 filesize = 0;
 551                 ntfs_inode *ni;
 552                 ntfs_attr_search_ctx *ctx = NULL;
 553                 FILE_NAME_ATTR *file_name_attr;
 554                 ATTR_RECORD *attr;
 555                 time_t ntfs_time;
 556                 char t_buf[26];
 557 
 558                 result = -1;                            // Everything else is bad
 559 
 560                 ni = ntfs_inode_open(dirent->vol, mref);
 561                 if (!ni)
 562                         goto release;
 563 
 564                 ctx = ntfs_attr_get_search_ctx(ni, NULL);
 565                 if (!ctx)
 566                         goto release;
 567 
 568                 if (ntfs_attr_lookup(AT_FILE_NAME, AT_UNNAMED, 0, 0, 0, NULL,
 569                                 0, ctx))
 570                         goto release;
 571                 attr = ctx->attr;
 572 
 573                 file_name_attr = (FILE_NAME_ATTR *)((char *)attr +
 574                                 le16_to_cpu(attr->u.res.value_offset));
 575                 if (!file_name_attr)
 576                         goto release;
 577 
 578                 ntfs_time = ntfs2utc(file_name_attr->last_data_change_time);
 579                 strcpy(t_buf, ctime(&ntfs_time));
 580                 memmove(t_buf+16, t_buf+19, 5);
 581                 t_buf[21] = '\0';
 582 
 583                 if (dt_type != NTFS_DT_DIR) {
 584                         if (!ntfs_attr_lookup(AT_DATA, AT_UNNAMED, 0, 0, 0,
 585                                         NULL, 0, ctx))
 586                                 filesize = ntfs_get_attribute_value_length(
 587                                                 ctx->attr);
 588                 }
 589 
 590                 if (opts.inode)
 591                         printf("%7llu    %8lld %s %s\n",
 592                                         (unsigned long long)MREF(mref),
 593                                         (long long)filesize, t_buf + 4,
 594                                         filename);
 595                 else
 596                         printf("%8lld %s %s\n", (long long)filesize, t_buf + 4,
 597                                         filename);
 598 
 599                 if (dir) {
 600                         dir->ni = ni;
 601                         ni = NULL;      /* so release does not close inode */
 602                 }
 603 
 604                 result = 0;
 605 release:
 606                 /* Release attribute search context and close the inode. */
 607                 if (ctx)
 608                         ntfs_attr_put_search_ctx(ctx);
 609                 if (ni)
 610                         ntfs_inode_close(ni);
 611         }
 612 
 613         if (dir) {
 614                 if (result == 0) {
 615                         list_add(&dir->list, dir_list_insert_pos);
 616                         dir_list_insert_pos = &dir->list;
 617                 } else {
 618                         free(dir);
 619                         dir = NULL;
 620                 }
 621         }
 622 
 623 free:
 624         free(filename);
 625         return result;
 626 }
 627 
 628 /**
 629  * main - Begin here
 630  *
 631  * Start from here.
 632  *
 633  * Return:  0  Success, the program worked
 634  *          1  Error, parsing mount options failed
 635  *          2  Error, mount attempt failed
 636  *          3  Error, failed to open root directory
 637  *          4  Error, failed to open directory in search path
 638  */
 639 int main(int argc, char **argv)
 640 {
 641         s64 pos;
 642         ntfs_volume *vol;
 643         ntfs_inode *ni;
 644         ntfsls_dirent dirent;
 645 
 646         ntfs_log_set_handler(ntfs_log_handler_outerr);
 647 
 648         if (!parse_options(argc, argv)) {
 649                 // FIXME: Print error... (AIA)
 650                 return 1;
 651         }
 652 
 653         utils_set_locale();
 654 
 655         vol = utils_mount_volume(opts.device, NTFS_MNT_RDONLY |
 656                         (opts.force ? NTFS_MNT_FORCE : 0));
 657         if (!vol) {
 658                 // FIXME: Print error... (AIA)
 659                 return 2;
 660         }
 661 
 662         ni = ntfs_pathname_to_inode(vol, NULL, opts.path);
 663         if (!ni) {
 664                 // FIXME: Print error... (AIA)
 665                 ntfs_umount(vol, FALSE);
 666                 return 3;
 667         }
 668 
 669         /*
 670          * We now are at the final path component.  If it is a file just
 671          * list it.  If it is a directory, list its contents.
 672          */
 673         pos = 0;
 674         memset(&dirent, 0, sizeof(dirent));
 675         dirent.vol = vol;
 676         if (ni->mrec->flags & MFT_RECORD_IS_DIRECTORY) {
 677                 if (opts.recursive)
 678                         readdir_recursive(ni, &pos, &dirent);
 679                 else
 680                         ntfs_readdir(ni, &pos, &dirent,
 681                                      (ntfs_filldir_t) list_dir_entry);
 682                 // FIXME: error checking... (AIA)
 683         } else {
 684                 ATTR_RECORD *rec;
 685                 FILE_NAME_ATTR *attr;
 686                 ntfs_attr_search_ctx *ctx;
 687                 int space = 4;
 688                 ntfschar *name = NULL;
 689                 int name_len = 0;;
 690 
 691                 ctx = ntfs_attr_get_search_ctx(ni, NULL);
 692                 if (!ctx)
 693                         return -1;
 694 
 695                 while ((rec = find_attribute(AT_FILE_NAME, ctx))) {
 696                         /* We know this will always be resident. */
 697                         attr = (FILE_NAME_ATTR *) ((char *) rec + le16_to_cpu(rec->u.res.value_offset));
 698 
 699                         if (attr->file_name_type < space) {
 700                                 name     = attr->file_name;
 701                                 name_len = attr->file_name_length;
 702                                 space    = attr->file_name_type;
 703                         }
 704                 }
 705 
 706                 list_dir_entry(&dirent, name, name_len, space, pos, ni->mft_no,
 707                                NTFS_DT_REG);
 708                 // FIXME: error checking... (AIA)
 709 
 710                 ntfs_attr_put_search_ctx(ctx);
 711         }
 712 
 713         /* Finished with the inode; release it. */
 714         ntfs_inode_close(ni);
 715 
 716         ntfs_umount(vol, FALSE);
 717         return 0;
 718 }
 719