1/*
2 * svnfsfs.c: FSFS repository manipulation tool main file.
3 *
4 * ====================================================================
5 *    Licensed to the Apache Software Foundation (ASF) under one
6 *    or more contributor license agreements.  See the NOTICE file
7 *    distributed with this work for additional information
8 *    regarding copyright ownership.  The ASF licenses this file
9 *    to you under the Apache License, Version 2.0 (the
10 *    "License"); you may not use this file except in compliance
11 *    with the License.  You may obtain a copy of the License at
12 *
13 *      http://www.apache.org/licenses/LICENSE-2.0
14 *
15 *    Unless required by applicable law or agreed to in writing,
16 *    software distributed under the License is distributed on an
17 *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
18 *    KIND, either express or implied.  See the License for the
19 *    specific language governing permissions and limitations
20 *    under the License.
21 * ====================================================================
22 */
23
24#include <apr_signal.h>
25
26#include "svn_pools.h"
27#include "svn_cmdline.h"
28#include "svn_opt.h"
29#include "svn_utf.h"
30#include "svn_path.h"
31#include "svn_dirent_uri.h"
32#include "svn_repos.h"
33#include "svn_cache_config.h"
34#include "svn_version.h"
35
36#include "private/svn_cmdline_private.h"
37
38#include "svn_private_config.h"
39
40#include "svnfsfs.h"
41
42
43/*** Code. ***/
44
45/* A flag to see if we've been cancelled by the client or not. */
46static volatile sig_atomic_t cancelled = FALSE;
47
48/* A signal handler to support cancellation. */
49static void
50signal_handler(int signum)
51{
52  apr_signal(signum, SIG_IGN);
53  cancelled = TRUE;
54}
55
56
57/* A helper to set up the cancellation signal handlers. */
58static void
59setup_cancellation_signals(void (*handler)(int signum))
60{
61  apr_signal(SIGINT, handler);
62#ifdef SIGBREAK
63  /* SIGBREAK is a Win32 specific signal generated by ctrl-break. */
64  apr_signal(SIGBREAK, handler);
65#endif
66#ifdef SIGHUP
67  apr_signal(SIGHUP, handler);
68#endif
69#ifdef SIGTERM
70  apr_signal(SIGTERM, handler);
71#endif
72}
73
74
75svn_error_t *
76check_cancel(void *baton)
77{
78  if (cancelled)
79    return svn_error_create(SVN_ERR_CANCELLED, NULL, _("Caught signal"));
80  else
81    return SVN_NO_ERROR;
82}
83
84
85/* Custom filesystem warning function. */
86static void
87warning_func(void *baton,
88             svn_error_t *err)
89{
90  if (! err)
91    return;
92  svn_handle_warning2(stderr, err, "svnfsfs: ");
93}
94
95
96/* Version compatibility check */
97static svn_error_t *
98check_lib_versions(void)
99{
100  static const svn_version_checklist_t checklist[] =
101    {
102      { "svn_subr",  svn_subr_version },
103      { "svn_repos", svn_repos_version },
104      { "svn_fs",    svn_fs_version },
105      { "svn_delta", svn_delta_version },
106      { NULL, NULL }
107    };
108  SVN_VERSION_DEFINE(my_version);
109
110  return svn_ver_check_list2(&my_version, checklist, svn_ver_equal);
111}
112
113
114
115/** Subcommands. **/
116
117enum svnfsfs__cmdline_options_t
118  {
119    svnfsfs__version = SVN_OPT_FIRST_LONGOPT_ID
120  };
121
122/* Option codes and descriptions.
123 *
124 * The entire list must be terminated with an entry of nulls.
125 */
126static const apr_getopt_option_t options_table[] =
127  {
128    {"help",          'h', 0,
129     N_("show help on a subcommand")},
130
131    {NULL,            '?', 0,
132     N_("show help on a subcommand")},
133
134    {"version",       svnfsfs__version, 0,
135     N_("show program version information")},
136
137    {"quiet",         'q', 0,
138     N_("no progress (only errors to stderr)")},
139
140    {"revision",      'r', 1,
141     N_("specify revision number ARG (or X:Y range)")},
142
143    {"memory-cache-size",     'M', 1,
144     N_("size of the extra in-memory cache in MB used to\n"
145        "                             minimize redundant operations. Default: 16.")},
146
147    {NULL}
148  };
149
150
151/* Array of available subcommands.
152 * The entire list must be terminated with an entry of nulls.
153 */
154static const svn_opt_subcommand_desc2_t cmd_table[] =
155{
156  {"help", subcommand__help, {"?", "h"}, N_
157   ("usage: svnfsfs help [SUBCOMMAND...]\n\n"
158    "Describe the usage of this program or its subcommands.\n"),
159   {0} },
160
161  {"dump-index", subcommand__dump_index, {0}, N_
162   ("usage: svnfsfs dump-index REPOS_PATH -r REV\n\n"
163    "Dump the index contents for the revision / pack file containing revision REV\n"
164    "to console.  This is only available for FSFS format 7 (SVN 1.9+) repositories.\n"
165    "The table produced contains a header in the first line followed by one line\n"
166    "per index entry, ordered by location in the revision / pack file.  Columns:\n\n"
167    "   * Byte offset (hex) at which the item starts\n"
168    "   * Length (hex) of the item in bytes\n"
169    "   * Item type (string) is one of the following:\n\n"
170    "        none ... Unused section.  File contents shall be NULs.\n"
171    "        frep ... File representation.\n"
172    "        drep ... Directory representation.\n"
173    "        fprop .. File property.\n"
174    "        dprop .. Directory property.\n"
175    "        node ... Node revision.\n"
176    "        chgs ... Changed paths list.\n"
177    "        rep .... Representation of unknown type.  Should not be used.\n"
178    "        ??? .... Invalid.  Index data is corrupt.\n\n"
179    "        The distinction between frep, drep, fprop and dprop is a mere internal\n"
180    "        classification used for various optimizations and does not affect the\n"
181    "        operational correctness.\n\n"
182    "   * Revision that the item belongs to (decimal)\n"
183    "   * Item number (decimal) within that revision\n"
184    "   * Modified FNV1a checksum (8 hex digits)\n"),
185   {'r', 'M'} },
186
187  {"load-index", subcommand__load_index, {0}, N_
188   ("usage: svnfsfs load-index REPOS_PATH\n\n"
189    "Read index contents from console.  The format is the same as produced by the\n"
190    "dump-index command, except that checksum as well as header are optional and will\n"
191    "be ignored.  The data must cover the full revision / pack file;  the revision\n"
192    "number is automatically extracted from input stream.  No ordering is required.\n"),
193   {'M'} },
194
195  {"stats", subcommand__stats, {0}, N_
196   ("usage: svnfsfs stats REPOS_PATH\n\n"
197    "Write object size statistics to console.\n"),
198   {'M'} },
199
200  { NULL, NULL, {0}, NULL, {0} }
201};
202
203
204svn_error_t *
205open_fs(svn_fs_t **fs,
206        const char *path,
207        apr_pool_t *pool)
208{
209  const char *fs_type;
210
211  /* Verify that we can handle the repository type. */
212  path = svn_dirent_join(path, "db", pool);
213  SVN_ERR(svn_fs_type(&fs_type, path, pool));
214  if (strcmp(fs_type, SVN_FS_TYPE_FSFS))
215    return svn_error_createf(SVN_ERR_FS_UNSUPPORTED_TYPE, NULL,
216                             _("%s repositories are not supported"),
217                             fs_type);
218
219  /* Now open it. */
220  SVN_ERR(svn_fs_open2(fs, path, NULL, pool, pool));
221  svn_fs_set_warning_func(*fs, warning_func, NULL);
222
223  return SVN_NO_ERROR;
224}
225
226/* This implements `svn_opt_subcommand_t'. */
227svn_error_t *
228subcommand__help(apr_getopt_t *os, void *baton, apr_pool_t *pool)
229{
230  svnfsfs__opt_state *opt_state = baton;
231  const char *header =
232    _("general usage: svnfsfs SUBCOMMAND REPOS_PATH  [ARGS & OPTIONS ...]\n"
233      "Subversion FSFS repository manipulation tool.\n"
234      "Type 'svnfsfs help <subcommand>' for help on a specific subcommand.\n"
235      "Type 'svnfsfs --version' to see the program version.\n"
236      "\n"
237      "Available subcommands:\n");
238
239  SVN_ERR(svn_opt_print_help4(os, "svnfsfs",
240                              opt_state ? opt_state->version : FALSE,
241                              opt_state ? opt_state->quiet : FALSE,
242                              /*###opt_state ? opt_state->verbose :*/ FALSE,
243                              NULL,
244                              header, cmd_table, options_table, NULL, NULL,
245                              pool));
246
247  return SVN_NO_ERROR;
248}
249
250
251/** Main. **/
252
253/*
254 * On success, leave *EXIT_CODE untouched and return SVN_NO_ERROR. On error,
255 * either return an error to be displayed, or set *EXIT_CODE to non-zero and
256 * return SVN_NO_ERROR.
257 */
258static svn_error_t *
259sub_main(int *exit_code, int argc, const char *argv[], apr_pool_t *pool)
260{
261  svn_error_t *err;
262  apr_status_t apr_err;
263
264  const svn_opt_subcommand_desc2_t *subcommand = NULL;
265  svnfsfs__opt_state opt_state = { 0 };
266  apr_getopt_t *os;
267  int opt_id;
268  apr_array_header_t *received_opts;
269  int i;
270
271  received_opts = apr_array_make(pool, SVN_OPT_MAX_OPTIONS, sizeof(int));
272
273  /* Check library versions */
274  SVN_ERR(check_lib_versions());
275
276  /* Initialize the FS library. */
277  SVN_ERR(svn_fs_initialize(pool));
278
279  if (argc <= 1)
280    {
281      SVN_ERR(subcommand__help(NULL, NULL, pool));
282      *exit_code = EXIT_FAILURE;
283      return SVN_NO_ERROR;
284    }
285
286  /* Initialize opt_state. */
287  opt_state.start_revision.kind = svn_opt_revision_unspecified;
288  opt_state.end_revision.kind = svn_opt_revision_unspecified;
289  opt_state.memory_cache_size = svn_cache_config_get()->cache_size;
290
291  /* Parse options. */
292  SVN_ERR(svn_cmdline__getopt_init(&os, argc, argv, pool));
293
294  os->interleave = 1;
295
296  while (1)
297    {
298      const char *opt_arg;
299      const char *utf8_opt_arg;
300
301      /* Parse the next option. */
302      apr_err = apr_getopt_long(os, options_table, &opt_id, &opt_arg);
303      if (APR_STATUS_IS_EOF(apr_err))
304        break;
305      else if (apr_err)
306        {
307          SVN_ERR(subcommand__help(NULL, NULL, pool));
308          *exit_code = EXIT_FAILURE;
309          return SVN_NO_ERROR;
310        }
311
312      /* Stash the option code in an array before parsing it. */
313      APR_ARRAY_PUSH(received_opts, int) = opt_id;
314
315      switch (opt_id) {
316      case 'r':
317        {
318          if (opt_state.start_revision.kind != svn_opt_revision_unspecified)
319            {
320              return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
321                        _("Multiple revision arguments encountered; "
322                          "try '-r N:M' instead of '-r N -r M'"));
323            }
324          if (svn_opt_parse_revision(&(opt_state.start_revision),
325                                     &(opt_state.end_revision),
326                                     opt_arg, pool) != 0)
327            {
328              SVN_ERR(svn_utf_cstring_to_utf8(&utf8_opt_arg, opt_arg, pool));
329
330              return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
331                        _("Syntax error in revision argument '%s'"),
332                        utf8_opt_arg);
333            }
334        }
335        break;
336      case 'q':
337        opt_state.quiet = TRUE;
338        break;
339      case 'h':
340      case '?':
341        opt_state.help = TRUE;
342        break;
343      case 'M':
344        opt_state.memory_cache_size
345            = 0x100000 * apr_strtoi64(opt_arg, NULL, 0);
346        break;
347      case svnfsfs__version:
348        opt_state.version = TRUE;
349        break;
350      default:
351        {
352          SVN_ERR(subcommand__help(NULL, NULL, pool));
353          *exit_code = EXIT_FAILURE;
354          return SVN_NO_ERROR;
355        }
356      }  /* close `switch' */
357    }  /* close `while' */
358
359  /* If the user asked for help, then the rest of the arguments are
360     the names of subcommands to get help on (if any), or else they're
361     just typos/mistakes.  Whatever the case, the subcommand to
362     actually run is subcommand_help(). */
363  if (opt_state.help)
364    subcommand = svn_opt_get_canonical_subcommand2(cmd_table, "help");
365
366  /* If we're not running the `help' subcommand, then look for a
367     subcommand in the first argument. */
368  if (subcommand == NULL)
369    {
370      if (os->ind >= os->argc)
371        {
372          if (opt_state.version)
373            {
374              /* Use the "help" subcommand to handle the "--version" option. */
375              static const svn_opt_subcommand_desc2_t pseudo_cmd =
376                { "--version", subcommand__help, {0}, "",
377                  {svnfsfs__version,  /* must accept its own option */
378                   'q',  /* --quiet */
379                  } };
380
381              subcommand = &pseudo_cmd;
382            }
383          else
384            {
385              svn_error_clear(svn_cmdline_fprintf(stderr, pool,
386                                        _("subcommand argument required\n")));
387              SVN_ERR(subcommand__help(NULL, NULL, pool));
388              *exit_code = EXIT_FAILURE;
389              return SVN_NO_ERROR;
390            }
391        }
392      else
393        {
394          const char *first_arg = os->argv[os->ind++];
395          subcommand = svn_opt_get_canonical_subcommand2(cmd_table, first_arg);
396          if (subcommand == NULL)
397            {
398              const char *first_arg_utf8;
399              SVN_ERR(svn_utf_cstring_to_utf8(&first_arg_utf8,
400                                                  first_arg, pool));
401              svn_error_clear(
402                svn_cmdline_fprintf(stderr, pool,
403                                    _("Unknown subcommand: '%s'\n"),
404                                    first_arg_utf8));
405              SVN_ERR(subcommand__help(NULL, NULL, pool));
406              *exit_code = EXIT_FAILURE;
407              return SVN_NO_ERROR;
408            }
409        }
410    }
411
412  /* Every subcommand except `help' requires a second argument -- the
413     repository path.  Parse it out here and store it in opt_state. */
414  if (!(subcommand->cmd_func == subcommand__help))
415    {
416      const char *repos_path = NULL;
417
418      if (os->ind >= os->argc)
419        {
420          return svn_error_create(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
421                                  _("Repository argument required"));
422        }
423
424      SVN_ERR(svn_utf_cstring_to_utf8(&repos_path, os->argv[os->ind++], pool));
425
426      if (svn_path_is_url(repos_path))
427        {
428          return svn_error_createf(SVN_ERR_CL_ARG_PARSING_ERROR, NULL,
429                                   _("'%s' is a URL when it should be a "
430                                     "local path"), repos_path);
431        }
432
433      opt_state.repository_path = svn_dirent_internal_style(repos_path, pool);
434    }
435
436  /* Check that the subcommand wasn't passed any inappropriate options. */
437  for (i = 0; i < received_opts->nelts; i++)
438    {
439      opt_id = APR_ARRAY_IDX(received_opts, i, int);
440
441      /* All commands implicitly accept --help, so just skip over this
442         when we see it. Note that we don't want to include this option
443         in their "accepted options" list because it would be awfully
444         redundant to display it in every commands' help text. */
445      if (opt_id == 'h' || opt_id == '?')
446        continue;
447
448      if (! svn_opt_subcommand_takes_option3(subcommand, opt_id, NULL))
449        {
450          const char *optstr;
451          const apr_getopt_option_t *badopt =
452            svn_opt_get_option_from_code2(opt_id, options_table, subcommand,
453                                          pool);
454          svn_opt_format_option(&optstr, badopt, FALSE, pool);
455          if (subcommand->name[0] == '-')
456            SVN_ERR(subcommand__help(NULL, NULL, pool));
457          else
458            svn_error_clear(svn_cmdline_fprintf(stderr, pool
459                            , _("Subcommand '%s' doesn't accept option '%s'\n"
460                                "Type 'svnfsfs help %s' for usage.\n"),
461                subcommand->name, optstr, subcommand->name));
462          *exit_code = EXIT_FAILURE;
463          return SVN_NO_ERROR;
464        }
465    }
466
467  /* Set up our cancellation support. */
468  setup_cancellation_signals(signal_handler);
469
470#ifdef SIGPIPE
471  /* Disable SIGPIPE generation for the platforms that have it. */
472  apr_signal(SIGPIPE, SIG_IGN);
473#endif
474
475#ifdef SIGXFSZ
476  /* Disable SIGXFSZ generation for the platforms that have it, otherwise
477   * working with large files when compiled against an APR that doesn't have
478   * large file support will crash the program, which is uncool. */
479  apr_signal(SIGXFSZ, SIG_IGN);
480#endif
481
482  /* Configure FSFS caches for maximum efficiency with svnfsfs.
483   * Also, apply the respective command line parameters, if given. */
484  {
485    svn_cache_config_t settings = *svn_cache_config_get();
486
487    settings.cache_size = opt_state.memory_cache_size;
488    settings.single_threaded = TRUE;
489
490    svn_cache_config_set(&settings);
491  }
492
493  /* Run the subcommand. */
494  err = (*subcommand->cmd_func)(os, &opt_state, pool);
495  if (err)
496    {
497      /* For argument-related problems, suggest using the 'help'
498         subcommand. */
499      if (err->apr_err == SVN_ERR_CL_INSUFFICIENT_ARGS
500          || err->apr_err == SVN_ERR_CL_ARG_PARSING_ERROR)
501        {
502          err = svn_error_quick_wrap(err,
503                                     _("Try 'svnfsfs help' for more info"));
504        }
505      return err;
506    }
507
508  return SVN_NO_ERROR;
509}
510
511int
512main(int argc, const char *argv[])
513{
514  apr_pool_t *pool;
515  int exit_code = EXIT_SUCCESS;
516  svn_error_t *err;
517
518  /* Initialize the app. */
519  if (svn_cmdline_init("svnfsfs", stderr) != EXIT_SUCCESS)
520    return EXIT_FAILURE;
521
522  /* Create our top-level pool.  Use a separate mutexless allocator,
523   * given this application is single threaded.
524   */
525  pool = apr_allocator_owner_get(svn_pool_create_allocator(FALSE));
526
527  err = sub_main(&exit_code, argc, argv, pool);
528
529  /* Flush stdout and report if it fails. It would be flushed on exit anyway
530     but this makes sure that output is not silently lost if it fails. */
531  err = svn_error_compose_create(err, svn_cmdline_fflush(stdout));
532
533  if (err)
534    {
535      exit_code = EXIT_FAILURE;
536      svn_cmdline_handle_exit_error(err, NULL, "svnfsfs: ");
537    }
538
539  svn_pool_destroy(pool);
540  return exit_code;
541}
542