root/source4/client/cifsdd.c

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. dd_handle_signal
  2. argtype_str
  3. find_named_arg
  4. set_arg_argv
  5. set_arg_val
  6. check_arg_bool
  7. check_arg_numeric
  8. check_arg_pathname
  9. dump_args
  10. cifsdd_help_message
  11. print_transfer_stats
  12. open_file
  13. copy_files
  14. main

   1 /*
   2    CIFSDD - dd for SMB.
   3    Main program, argument handling and block copying.
   4 
   5    Copyright (C) James Peach 2005-2006
   6 
   7    This program is free software; you can redistribute it and/or modify
   8    it under the terms of the GNU General Public License as published by
   9    the Free Software Foundation; either version 3 of the License, or
  10    (at your option) any later version.
  11 
  12    This program is distributed in the hope that it will be useful,
  13    but WITHOUT ANY WARRANTY; without even the implied warranty of
  14    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  15    GNU General Public License for more details.
  16 
  17    You should have received a copy of the GNU General Public License
  18    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  19 */
  20 
  21 #include "includes.h"
  22 #include "system/filesys.h"
  23 #include "auth/gensec/gensec.h"
  24 #include "lib/cmdline/popt_common.h"
  25 #include "libcli/resolve/resolve.h"
  26 #include "libcli/raw/libcliraw.h"
  27 #include "lib/events/events.h"
  28 
  29 #include "cifsdd.h"
  30 #include "param/param.h"
  31 
  32 const char * const PROGNAME = "cifsdd";
  33 
  34 #define SYNTAX_EXIT_CODE         1      /* Invokation syntax or logic error. */
  35 #define EOM_EXIT_CODE            9      /* Out of memory error. */
  36 #define FILESYS_EXIT_CODE       10      /* File manipulation error. */
  37 #define IOERROR_EXIT_CODE       11      /* Error during IO phase. */
  38 
  39 struct  dd_stats_record dd_stats;
  40 
  41 static int dd_sigint;
  42 static int dd_sigusr1;
  43 
  44 static void dd_handle_signal(int sig)
     /* [<][>][^][v][top][bottom][index][help] */
  45 {
  46         switch (sig)
  47         {
  48                 case SIGINT:
  49                         ++dd_sigint;
  50                         break;
  51                 case SIGUSR1:
  52                         ++dd_sigusr1;
  53                         break;
  54                 default:
  55                         break;
  56         }
  57 }
  58 
  59 /* ------------------------------------------------------------------------- */
  60 /* Argument handling.                                                        */
  61 /* ------------------------------------------------------------------------- */
  62 
  63 static const char * argtype_str(enum argtype arg_type)
     /* [<][>][^][v][top][bottom][index][help] */
  64 {
  65         static const struct {
  66                 enum argtype arg_type;
  67                 const char * arg_name;
  68         } names [] = 
  69         {
  70                 { ARG_NUMERIC, "COUNT" },
  71                 { ARG_SIZE, "SIZE" },
  72                 { ARG_PATHNAME, "FILE" },
  73                 { ARG_BOOL, "BOOLEAN" },
  74         };
  75 
  76         int i;
  77 
  78         for (i = 0; i < ARRAY_SIZE(names); ++i) {
  79                 if (arg_type == names[i].arg_type) {
  80                         return(names[i].arg_name);
  81                 }
  82         }
  83 
  84         return("<unknown>");
  85 }
  86 
  87 static struct argdef args[] =
  88 {
  89         { "bs", ARG_SIZE,       "force ibs and obs to SIZE bytes" },
  90         { "ibs", ARG_SIZE,      "read SIZE bytes at a time" },
  91         { "obs", ARG_SIZE,      "write SIZE bytes at a time" },
  92 
  93         { "count", ARG_NUMERIC, "copy COUNT input blocks" },
  94         { "seek",ARG_NUMERIC,   "skip COUNT blocks at start of output" },
  95         { "skip",ARG_NUMERIC,   "skip COUNT blocks at start of input" },
  96 
  97         { "if", ARG_PATHNAME,   "read input from FILE" },
  98         { "of", ARG_PATHNAME,   "write output to FILE" },
  99 
 100         { "direct", ARG_BOOL,   "use direct I/O if non-zero" },
 101         { "sync", ARG_BOOL,     "use synchronous writes if non-zero" },
 102         { "oplock", ARG_BOOL,   "take oplocks on the input and output files" },
 103 
 104 /* FIXME: We should support using iflags and oflags for setting oplock and I/O
 105  * options. This would make us compatible with GNU dd.
 106  */
 107 };
 108 
 109 static struct argdef * find_named_arg(const char * arg)
     /* [<][>][^][v][top][bottom][index][help] */
 110 {
 111         int i;
 112 
 113         for (i = 0; i < ARRAY_SIZE(args); ++i) {
 114                 if (strwicmp(arg, args[i].arg_name) == 0) {
 115                         return(&args[i]);
 116                 }
 117         }
 118 
 119         return(NULL);
 120 }
 121 
 122 int set_arg_argv(const char * argv)
     /* [<][>][^][v][top][bottom][index][help] */
 123 {
 124         struct argdef * arg;
 125 
 126         char *  name;
 127         char *  val;
 128 
 129         if ((name = strdup(argv)) == NULL) {
 130                 return(0);
 131         }
 132 
 133         if ((val = strchr(name, '=')) == NULL) {
 134                 fprintf(stderr, "%s: malformed argument \"%s\"\n",
 135                                 PROGNAME, argv);
 136                 goto fail;
 137         }
 138 
 139         *val = '\0';
 140         val++;
 141 
 142         if ((arg = find_named_arg(name)) == NULL) {
 143                 fprintf(stderr, "%s: ignoring unknown argument \"%s\"\n",
 144                                 PROGNAME, name);
 145                 goto fail;
 146         }
 147 
 148         /* Found a matching name; convert the variable argument. */
 149         switch (arg->arg_type) {
 150                 case ARG_NUMERIC:
 151                         if (!conv_str_u64(val, &arg->arg_val.nval)) {
 152                                 goto fail;
 153                         }
 154                         break;
 155                 case ARG_SIZE:
 156                         if (!conv_str_size(val, &arg->arg_val.nval)) {
 157                                 goto fail;
 158                         }
 159                         break;
 160                 case ARG_BOOL:
 161                         if (!conv_str_bool(val, &arg->arg_val.bval)) {
 162                                 goto fail;
 163                         }
 164                         break;
 165                 case ARG_PATHNAME:
 166                         if (!(arg->arg_val.pval = strdup(val))) {
 167                                 goto fail;
 168                         }
 169                         break;
 170                 default:
 171                         fprintf(stderr, "%s: argument \"%s\" is of "
 172                                 "unknown type\n", PROGNAME, name);
 173                         goto fail;
 174         }
 175 
 176         free(name);
 177         return(1);
 178 
 179 fail:
 180         free(name);
 181         return(0);
 182 }
 183 
 184 void set_arg_val(const char * name, ...)
     /* [<][>][^][v][top][bottom][index][help] */
 185 {
 186         va_list         ap;
 187         struct argdef * arg;
 188 
 189         va_start(ap, name);
 190         if ((arg = find_named_arg(name)) == NULL) {
 191                 goto fail;
 192         }
 193 
 194         /* Found a matching name; convert the variable argument. */
 195         switch (arg->arg_type) {
 196                 case ARG_NUMERIC:
 197                 case ARG_SIZE:
 198                         arg->arg_val.nval = va_arg(ap, uint64_t);
 199                         break;
 200                 case ARG_BOOL:
 201                         arg->arg_val.bval = va_arg(ap, int);
 202                         break;
 203                 case ARG_PATHNAME:
 204                         arg->arg_val.pval = va_arg(ap, char *);
 205                         if (arg->arg_val.pval) {
 206                                 arg->arg_val.pval = strdup(arg->arg_val.pval);
 207                         }
 208                         break;
 209                 default:
 210                         fprintf(stderr, "%s: argument \"%s\" is of "
 211                                 "unknown type\n", PROGNAME, name);
 212                         goto fail;
 213         }
 214 
 215         va_end(ap);
 216         return;
 217 
 218 fail:
 219         fprintf(stderr, "%s: ignoring unknown argument \"%s\"\n",
 220                         PROGNAME, name);
 221         va_end(ap);
 222         return;
 223 }
 224 
 225 bool check_arg_bool(const char * name)
     /* [<][>][^][v][top][bottom][index][help] */
 226 {
 227         struct argdef * arg;
 228 
 229         if ((arg = find_named_arg(name)) &&
 230             (arg->arg_type == ARG_BOOL)) {
 231                 return(arg->arg_val.bval);
 232         }
 233 
 234         DEBUG(0, ("invalid argument name: %s", name));
 235         SMB_ASSERT(0);
 236         return(false);
 237 }
 238 
 239 uint64_t check_arg_numeric(const char * name)
     /* [<][>][^][v][top][bottom][index][help] */
 240 {
 241         struct argdef * arg;
 242 
 243         if ((arg = find_named_arg(name)) &&
 244             (arg->arg_type == ARG_NUMERIC || arg->arg_type == ARG_SIZE)) {
 245                 return(arg->arg_val.nval);
 246         }
 247 
 248         DEBUG(0, ("invalid argument name: %s", name));
 249         SMB_ASSERT(0);
 250         return(-1);
 251 }
 252 
 253 const char * check_arg_pathname(const char * name)
     /* [<][>][^][v][top][bottom][index][help] */
 254 {
 255         struct argdef * arg;
 256 
 257         if ((arg = find_named_arg(name)) &&
 258             (arg->arg_type == ARG_PATHNAME)) {
 259                 return(arg->arg_val.pval);
 260         }
 261 
 262         DEBUG(0, ("invalid argument name: %s", name));
 263         SMB_ASSERT(0);
 264         return(NULL);
 265 }
 266 
 267 static void dump_args(void)
     /* [<][>][^][v][top][bottom][index][help] */
 268 {
 269         int i;
 270 
 271         DEBUG(10, ("dumping argument values:\n"));
 272         for (i = 0; i < ARRAY_SIZE(args); ++i) {
 273                 switch (args[i].arg_type) {
 274                         case ARG_NUMERIC:
 275                         case ARG_SIZE:
 276                                 DEBUG(10, ("\t%s=%llu\n", args[i].arg_name,
 277                                         (unsigned long long)args[i].arg_val.nval));
 278                                 break;
 279                         case ARG_BOOL:
 280                                 DEBUG(10, ("\t%s=%s\n", args[i].arg_name,
 281                                         args[i].arg_val.bval ? "yes" : "no"));
 282                                 break;
 283                         case ARG_PATHNAME:
 284                                 DEBUG(10, ("\t%s=%s\n", args[i].arg_name,
 285                                         args[i].arg_val.pval ?
 286                                                 args[i].arg_val.pval :
 287                                                 "(NULL)"));
 288                                 break;
 289                         default:
 290                                 SMB_ASSERT(0);
 291                 }
 292         }
 293 }
 294 
 295 static void cifsdd_help_message(poptContext pctx,
     /* [<][>][^][v][top][bottom][index][help] */
 296                 enum poptCallbackReason preason,
 297                 struct poptOption * poption, 
 298                 const char * parg,
 299                 void * pdata)
 300 {
 301         static const char notes[] = 
 302 "FILE can be a local filename or a UNC path of the form //server/share/path.\n";
 303 
 304         char prefix[24];
 305         int i;
 306 
 307         if (poption->shortName != '?') {
 308                 poptPrintUsage(pctx, stdout, 0);
 309                 fprintf(stdout, "        [dd options]\n");
 310                 exit(0);
 311         }
 312 
 313         poptPrintHelp(pctx, stdout, 0);
 314         fprintf(stdout, "\nCIFS dd options:\n");
 315 
 316         for (i = 0; i < ARRAY_SIZE(args); ++i) {
 317                 if (args[i].arg_name == NULL) {
 318                         break;
 319                 }
 320 
 321                 snprintf(prefix, sizeof(prefix), "%s=%-*s",
 322                         args[i].arg_name,
 323                         (int)(sizeof(prefix) - strlen(args[i].arg_name) - 2),
 324                         argtype_str(args[i].arg_type));
 325                 prefix[sizeof(prefix) - 1] = '\0';
 326                 fprintf(stdout, "  %s%s\n", prefix, args[i].arg_help);
 327         }
 328 
 329         fprintf(stdout, "\n%s\n", notes);
 330         exit(0);
 331 }
 332 
 333 /* ------------------------------------------------------------------------- */
 334 /* Main block copying routine.                                               */
 335 /* ------------------------------------------------------------------------- */
 336 
 337 static void print_transfer_stats(void)
     /* [<][>][^][v][top][bottom][index][help] */
 338 {
 339         if (DEBUGLEVEL > 0) {
 340                 printf("%llu+%llu records in (%llu bytes)\n"
 341                         "%llu+%llu records out (%llu bytes)\n",
 342                         (unsigned long long)dd_stats.in.fblocks,
 343                         (unsigned long long)dd_stats.in.pblocks,
 344                         (unsigned long long)dd_stats.in.bytes,
 345                         (unsigned long long)dd_stats.out.fblocks,
 346                         (unsigned long long)dd_stats.out.pblocks,
 347                         (unsigned long long)dd_stats.out.bytes);
 348         } else {
 349                 printf("%llu+%llu records in\n%llu+%llu records out\n",
 350                                 (unsigned long long)dd_stats.in.fblocks,
 351                                 (unsigned long long)dd_stats.in.pblocks,
 352                                 (unsigned long long)dd_stats.out.fblocks,
 353                                 (unsigned long long)dd_stats.out.pblocks);
 354         }
 355 }
 356 
 357 static struct dd_iohandle * open_file(struct resolve_context *resolve_ctx, 
     /* [<][>][^][v][top][bottom][index][help] */
 358                                       struct tevent_context *ev,
 359                                       const char * which, const char **ports,
 360                                       struct smbcli_options *smb_options,
 361                                       const char *socket_options,
 362                                       struct smbcli_session_options *smb_session_options,
 363                                       struct smb_iconv_convenience *iconv_convenience,
 364                                       struct gensec_settings *gensec_settings)
 365 {
 366         int                     options = 0;
 367         const char *            path = NULL;
 368         struct dd_iohandle *    handle = NULL;
 369 
 370         if (check_arg_bool("direct")) {
 371                 options |= DD_DIRECT_IO;
 372         }
 373 
 374         if (check_arg_bool("sync")) {
 375                 options |= DD_SYNC_IO;
 376         }
 377 
 378         if (check_arg_bool("oplock")) {
 379                 options |= DD_OPLOCK;
 380         }
 381 
 382         if (strcmp(which, "if") == 0) {
 383                 path = check_arg_pathname("if");
 384                 handle = dd_open_path(resolve_ctx, ev, path, ports,
 385                                       check_arg_numeric("ibs"), options,
 386                                       socket_options,
 387                                       smb_options, smb_session_options,
 388                                       iconv_convenience,
 389                                       gensec_settings);
 390         } else if (strcmp(which, "of") == 0) {
 391                 options |= DD_WRITE;
 392                 path = check_arg_pathname("of");
 393                 handle = dd_open_path(resolve_ctx, ev, path, ports,
 394                                       check_arg_numeric("obs"), options,
 395                                       socket_options,
 396                                       smb_options, smb_session_options,
 397                                       iconv_convenience,
 398                                       gensec_settings);
 399         } else {
 400                 SMB_ASSERT(0);
 401                 return(NULL);
 402         }
 403 
 404         if (!handle) {
 405                 fprintf(stderr, "%s: failed to open %s\n", PROGNAME, path);
 406         }
 407 
 408         return(handle);
 409 }
 410 
 411 static int copy_files(struct tevent_context *ev, struct loadparm_context *lp_ctx)
     /* [<][>][^][v][top][bottom][index][help] */
 412 {
 413         uint8_t *       iobuf;  /* IO buffer. */
 414         uint64_t        iomax;  /* Size of the IO buffer. */
 415         uint64_t        data_size; /* Amount of data in the IO buffer. */
 416 
 417         uint64_t        ibs;
 418         uint64_t        obs;
 419         uint64_t        count;
 420 
 421         struct dd_iohandle *    ifile;
 422         struct dd_iohandle *    ofile;
 423 
 424         struct smbcli_options options;
 425         struct smbcli_session_options session_options;
 426 
 427         ibs = check_arg_numeric("ibs");
 428         obs = check_arg_numeric("obs");
 429         count = check_arg_numeric("count");
 430 
 431         lp_smbcli_options(lp_ctx, &options);
 432         lp_smbcli_session_options(lp_ctx, &session_options);
 433 
 434         /* Allocate IO buffer. We need more than the max IO size because we
 435          * could accumulate a remainder if ibs and obs don't match.
 436          */
 437         iomax = 2 * MAX(ibs, obs);
 438         if ((iobuf = malloc_array_p(uint8_t, iomax)) == NULL) {
 439                 fprintf(stderr,
 440                         "%s: failed to allocate IO buffer of %llu bytes\n",
 441                         PROGNAME, (unsigned long long)iomax);
 442                 return(EOM_EXIT_CODE);
 443         }
 444 
 445         options.max_xmit = MAX(ibs, obs);
 446 
 447         DEBUG(4, ("IO buffer size is %llu, max xmit is %d\n",
 448                         (unsigned long long)iomax, options.max_xmit));
 449 
 450         if (!(ifile = open_file(lp_resolve_context(lp_ctx), ev, "if",
 451                                 lp_smb_ports(lp_ctx), &options,
 452                                 lp_socket_options(lp_ctx),
 453                                 &session_options, lp_iconv_convenience(lp_ctx),
 454                                 lp_gensec_settings(lp_ctx, lp_ctx)))) {
 455                 return(FILESYS_EXIT_CODE);
 456         }
 457 
 458         if (!(ofile = open_file(lp_resolve_context(lp_ctx), ev, "of",
 459                                 lp_smb_ports(lp_ctx), &options,
 460                                 lp_socket_options(lp_ctx),
 461                                 &session_options,
 462                                 lp_iconv_convenience(lp_ctx),
 463                                 lp_gensec_settings(lp_ctx, lp_ctx)))) {
 464                 return(FILESYS_EXIT_CODE);
 465         }
 466 
 467         /* Seek the files to their respective starting points. */
 468         ifile->io_seek(ifile, check_arg_numeric("skip") * ibs);
 469         ofile->io_seek(ofile, check_arg_numeric("seek") * obs);
 470 
 471         DEBUG(4, ("max xmit was negotiated to be %d\n", options.max_xmit));
 472 
 473         for (data_size = 0;;) {
 474 
 475                 /* Handle signals. We are somewhat compatible with GNU dd.
 476                  * SIGINT makes us stop, but still print transfer statistics.
 477                  * SIGUSR1 makes us print transfer statistics but we continue
 478                  * copying.
 479                  */
 480                 if (dd_sigint) {
 481                         break;
 482                 }
 483 
 484                 if (dd_sigusr1) {
 485                         print_transfer_stats();
 486                         dd_sigusr1 = 0;
 487                 }
 488 
 489                 if (ifile->io_flags & DD_END_OF_FILE) {
 490                         DEBUG(4, ("flushing %llu bytes at EOF\n",
 491                                         (unsigned long long)data_size));
 492                         while (data_size > 0) {
 493                                 if (!dd_flush_block(ofile, iobuf,
 494                                                         &data_size, obs)) {
 495                                         return(IOERROR_EXIT_CODE);
 496                                 }
 497                         }
 498                         goto done;
 499                 }
 500 
 501                 /* Try and read enough blocks of ibs bytes to be able write
 502                  * out one of obs bytes.
 503                  */
 504                 if (!dd_fill_block(ifile, iobuf, &data_size, obs, ibs)) {
 505                         return(IOERROR_EXIT_CODE);
 506                 }
 507 
 508                 if (data_size == 0) {
 509                         /* Done. */
 510                         SMB_ASSERT(ifile->io_flags & DD_END_OF_FILE);
 511                 }
 512 
 513                 /* Stop reading when we hit the block count. */
 514                 if (dd_stats.in.bytes >= (ibs * count)) {
 515                         ifile->io_flags |= DD_END_OF_FILE;
 516                 }
 517 
 518                 /* If we wanted to be a legitimate dd, we would do character
 519                  * conversions and other shenanigans here.
 520                  */
 521 
 522                 /* Flush what we read in units of obs bytes. We want to have
 523                  * at least obs bytes in the IO buffer but might not if the
 524                  * file is too small.
 525                  */
 526                 if (data_size && 
 527                     !dd_flush_block(ofile, iobuf, &data_size, obs)) {
 528                         return(IOERROR_EXIT_CODE);
 529                 }
 530         }
 531 
 532 done:
 533         print_transfer_stats();
 534         return(0);
 535 }
 536 
 537 /* ------------------------------------------------------------------------- */
 538 /* Main.                                                                     */
 539 /* ------------------------------------------------------------------------- */
 540 
 541 struct poptOption cifsddHelpOptions[] = {
 542   { NULL, '\0', POPT_ARG_CALLBACK, (void *)&cifsdd_help_message, '\0', NULL, NULL },
 543   { "help", '?', 0, NULL, '?', "Show this help message", NULL },
 544   { "usage", '\0', 0, NULL, 'u', "Display brief usage message", NULL },
 545   { NULL }
 546 } ;
 547 
 548 int main(int argc, const char ** argv)
     /* [<][>][^][v][top][bottom][index][help] */
 549 {
 550         int i;
 551         const char ** dd_args;
 552         struct tevent_context *ev;
 553 
 554         poptContext pctx;
 555         struct poptOption poptions[] = {
 556                 /* POPT_AUTOHELP */
 557                 { NULL, '\0', POPT_ARG_INCLUDE_TABLE, cifsddHelpOptions,
 558                         0, "Help options:", NULL },
 559                 POPT_COMMON_SAMBA
 560                 POPT_COMMON_CONNECTION
 561                 POPT_COMMON_CREDENTIALS
 562                 POPT_COMMON_VERSION
 563                 { NULL }
 564         };
 565 
 566         /* Block sizes. */
 567         set_arg_val("bs", (uint64_t)4096);
 568         set_arg_val("ibs", (uint64_t)4096);
 569         set_arg_val("obs", (uint64_t)4096);
 570         /* Block counts. */
 571         set_arg_val("count", (uint64_t)-1);
 572         set_arg_val("seek", (uint64_t)0);
 573         set_arg_val("seek", (uint64_t)0);
 574         /* Files. */
 575         set_arg_val("if", NULL);
 576         set_arg_val("of", NULL);
 577         /* Options. */
 578         set_arg_val("direct", false);
 579         set_arg_val("sync", false);
 580         set_arg_val("oplock", false);
 581 
 582         pctx = poptGetContext(PROGNAME, argc, argv, poptions, 0);
 583         while ((i = poptGetNextOpt(pctx)) != -1) {
 584                 ;
 585         }
 586 
 587         for (dd_args = poptGetArgs(pctx); dd_args && *dd_args; ++dd_args) {
 588 
 589                 if (!set_arg_argv(*dd_args)) {
 590                         fprintf(stderr, "%s: invalid option: %s\n",
 591                                         PROGNAME, *dd_args);
 592                         exit(SYNTAX_EXIT_CODE);
 593                 }
 594 
 595                 /* "bs" has the side-effect of setting "ibs" and "obs". */
 596                 if (strncmp(*dd_args, "bs=", 3) == 0) {
 597                         uint64_t bs = check_arg_numeric("bs");
 598                         set_arg_val("ibs", bs);
 599                         set_arg_val("obs", bs);
 600                 }
 601         }
 602 
 603         ev = s4_event_context_init(talloc_autofree_context());
 604 
 605         gensec_init(cmdline_lp_ctx);
 606         dump_args();
 607 
 608         if (check_arg_numeric("ibs") == 0 || check_arg_numeric("ibs") == 0) {
 609                 fprintf(stderr, "%s: block sizes must be greater that zero\n",
 610                                 PROGNAME);
 611                 exit(SYNTAX_EXIT_CODE);
 612         }
 613 
 614         if (check_arg_pathname("if") == NULL) {
 615                 fprintf(stderr, "%s: missing input filename\n", PROGNAME);
 616                 exit(SYNTAX_EXIT_CODE);
 617         }
 618 
 619         if (check_arg_pathname("of") == NULL) {
 620                 fprintf(stderr, "%s: missing output filename\n", PROGNAME);
 621                 exit(SYNTAX_EXIT_CODE);
 622         }
 623 
 624         CatchSignal(SIGINT, dd_handle_signal);
 625         CatchSignal(SIGUSR1, dd_handle_signal);
 626         return(copy_files(ev, cmdline_lp_ctx));
 627 }
 628 
 629 /* vim: set sw=8 sts=8 ts=8 tw=79 : */

/* [<][>][^][v][top][bottom][index][help] */