| /* |
| * config_file.c : parsing configuration files |
| * |
| * ==================================================================== |
| * Copyright (c) 2000-2002 CollabNet. All rights reserved. |
| * |
| * This software is licensed as described in the file COPYING, which |
| * you should have received as part of this distribution. The terms |
| * are also available at http://subversion.tigris.org/license-1.html. |
| * If newer versions of this license are posted there, you may use a |
| * newer version instead, at your option. |
| * |
| * This software consists of voluntary contributions made by many |
| * individuals. For exact contribution history, see the revision |
| * history and logs, available at http://subversion.tigris.org/. |
| * ==================================================================== |
| */ |
| |
| |
| |
| #define APR_WANT_STDIO |
| #include <apr_want.h> |
| |
| #include <apr_lib.h> |
| #include "config_impl.h" |
| #include "svn_io.h" |
| #include "svn_types.h" |
| |
| |
| /* File parsing context */ |
| typedef struct parse_context_t |
| { |
| /* This config struct and file */ |
| svn_config_t *cfg; |
| const char *file; |
| |
| /* The file descriptor */ |
| FILE *fd; |
| |
| /* The current line in the file */ |
| int line; |
| |
| /* Temporary strings, allocated from the temp pool */ |
| svn_stringbuf_t *section; |
| svn_stringbuf_t *option; |
| svn_stringbuf_t *value; |
| |
| /* Temporary pool parsing */ |
| apr_pool_t *pool; |
| } parse_context_t; |
| |
| |
| /* Eat chars from FD until encounter non-whitespace, newline, or EOF. |
| Set *PCOUNT to the number of characters eaten, not counting the |
| last one, and return the last char read (the one that caused the |
| break). */ |
| static APR_INLINE int |
| skip_whitespace (FILE* fd, int *pcount) |
| { |
| int ch = getc (fd); |
| int count = 0; |
| while (ch != EOF && ch != '\n' && apr_isspace (ch)) |
| { |
| ++count; |
| ch = getc (fd); |
| } |
| *pcount = count; |
| return ch; |
| } |
| |
| |
| /* Skip to the end of the line (or file). Returns the char that ended |
| the line; the char is either EOF or newline. */ |
| static APR_INLINE int |
| skip_to_eoln (FILE *fd) |
| { |
| int ch = getc (fd); |
| while (ch != EOF && ch != '\n') |
| ch = getc (fd); |
| return ch; |
| } |
| |
| |
| /* Parse a single option value */ |
| static svn_error_t * |
| parse_value (int *pch, parse_context_t *ctx) |
| { |
| svn_error_t *err = SVN_NO_ERROR; |
| svn_boolean_t end_of_val = FALSE; |
| int ch; |
| |
| /* Read the first line of the value */ |
| svn_stringbuf_setempty (ctx->value); |
| for (ch = getc (ctx->fd); /* last ch seen was ':' or '=' in parse_option. */ |
| ch != EOF && ch != '\n'; |
| ch = getc (ctx->fd)) |
| { |
| const char char_from_int = ch; |
| svn_stringbuf_appendbytes (ctx->value, &char_from_int, 1); |
| } |
| /* Leading and trailing whitespace is ignored. */ |
| svn_stringbuf_strip_whitespace (ctx->value); |
| |
| /* Look for any continuation lines. */ |
| for (;;) |
| { |
| if (ch == EOF || end_of_val) |
| { |
| if (!ferror (ctx->fd)) |
| { |
| /* At end of file. The value is complete, there can't be |
| any continuation lines. */ |
| svn_config_set (ctx->cfg, ctx->section->data, |
| ctx->option->data, ctx->value->data); |
| } |
| break; |
| } |
| else |
| { |
| int count; |
| ++ctx->line; |
| ch = skip_whitespace (ctx->fd, &count); |
| |
| switch (ch) |
| { |
| case '\n': |
| /* The next line was empty. Ergo, it can't be a |
| continuation line. */ |
| ++ctx->line; |
| end_of_val = TRUE; |
| continue; |
| |
| case EOF: |
| /* This is also an empty line. */ |
| end_of_val = TRUE; |
| continue; |
| |
| default: |
| if (count == 0) |
| { |
| /* This line starts in the first column. That means |
| it's either a section, option or comment. Put |
| the char back into the stream, because it doesn't |
| belong to us. */ |
| ungetc (ch, ctx->fd); |
| end_of_val = TRUE; |
| } |
| else |
| { |
| /* This is a continuation line. Read it. */ |
| svn_stringbuf_appendbytes (ctx->value, " ", 1); |
| |
| for (; |
| ch != EOF && ch != '\n'; |
| ch = getc (ctx->fd)) |
| { |
| const char char_from_int = ch; |
| svn_stringbuf_appendbytes (ctx->value, |
| &char_from_int, 1); |
| } |
| /* Trailing whitespace is ignored. */ |
| svn_stringbuf_strip_whitespace (ctx->value); |
| } |
| } |
| } |
| } |
| |
| *pch = ch; |
| return err; |
| } |
| |
| |
| /* Parse a single option */ |
| static svn_error_t * |
| parse_option (int *pch, parse_context_t *ctx) |
| { |
| svn_error_t *err = SVN_NO_ERROR; |
| int ch; |
| |
| svn_stringbuf_setempty (ctx->option); |
| for (ch = *pch; /* Yes, the first char is relevant. */ |
| ch != EOF && ch != ':' && ch != '=' && ch != '\n'; |
| ch = getc (ctx->fd)) |
| { |
| const char char_from_int = ch; |
| svn_stringbuf_appendbytes (ctx->option, &char_from_int, 1); |
| } |
| |
| if (ch != ':' && ch != '=') |
| { |
| ch = EOF; |
| err = svn_error_createf (SVN_ERR_MALFORMED_FILE, |
| 0, NULL, ctx->pool, |
| "%s:%d: Option must end with ':' or '='", |
| ctx->file, ctx->line); |
| } |
| else |
| { |
| /* Whitespace around the name separator is ignored. */ |
| svn_stringbuf_strip_whitespace (ctx->option); |
| err = parse_value (&ch, ctx); |
| } |
| |
| *pch = ch; |
| return err; |
| } |
| |
| |
| /* Read chars until enounter ']', then skip everything to the end of |
| * the line. Set *PCH to the character that ended the line (either |
| * newline or EOF), and set CTX->section to the string of characters |
| * seen before ']'. |
| * |
| * This is meant to be called immediately after reading the '[' that |
| * starts a section name. |
| */ |
| static svn_error_t * |
| parse_section_name (int *pch, parse_context_t *ctx) |
| { |
| svn_error_t *err = SVN_NO_ERROR; |
| int ch; |
| |
| svn_stringbuf_setempty (ctx->section); |
| for (ch = getc (ctx->fd); |
| ch != EOF && ch != ']' && ch != '\n'; |
| ch = getc (ctx->fd)) |
| { |
| const char char_from_int = ch; |
| svn_stringbuf_appendbytes (ctx->section, &char_from_int, 1); |
| } |
| |
| if (ch != ']') |
| { |
| ch = EOF; |
| err = svn_error_createf (SVN_ERR_MALFORMED_FILE, |
| 0, NULL, ctx->pool, |
| "%s:%d: Section header must end with ']'", |
| ctx->file, ctx->line); |
| } |
| else |
| { |
| /* Everything from the ']' to the end of the line is ignored. */ |
| ch = skip_to_eoln (ctx->fd); |
| if (ch != EOF) |
| ++ctx->line; |
| } |
| |
| *pch = ch; |
| return err; |
| } |
| |
| |
| svn_error_t * |
| svn_config__sys_config_path (const char **path_p, |
| const char *fname, |
| apr_pool_t *pool) |
| { |
| #ifdef SVN_WIN32 |
| |
| /* ### See http://subversion.tigris.org/issues/show_bug.cgi?id=579 |
| for more on how this will be done. */ |
| *path_p = NULL; |
| |
| #else /* ! SVN_WIN32 */ |
| |
| /* No reason to use svn's path lib here; we know what the separator |
| is in this case. */ |
| if (fname) |
| *path_p = apr_psprintf (pool, "%s/%s", SVN_CONFIG__SYS_DIRECTORY, fname); |
| else |
| *path_p = SVN_CONFIG__SYS_DIRECTORY; |
| |
| #endif /* SVN_WIN32 */ |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| svn_error_t * |
| svn_config__user_config_path (const char **path_p, |
| const char *fname, |
| apr_pool_t *pool) |
| { |
| apr_status_t apr_err; |
| apr_uid_t uid; |
| apr_gid_t gid; |
| char *username; |
| char *homedir; |
| |
| /* ### See http://subversion.tigris.org/issues/show_bug.cgi?id=579 |
| for details on how to make this function meaningful under Win32. |
| Most likely strategy is to divide it into Win32 and non-Win32 |
| sections, as with svn_config__user_config_path() above. */ |
| |
| /* This code requires APR_HAS_USER to be defined. Does anyone not |
| define it? Apparently even Win32 does, though functions about |
| users may or may not return useful results there. */ |
| |
| *path_p = NULL; |
| |
| apr_err = apr_current_userid (&uid, &gid, pool); |
| if (apr_err) |
| return SVN_NO_ERROR; |
| |
| apr_err = apr_get_username (&username, uid, pool); |
| if (apr_err) |
| return SVN_NO_ERROR; |
| |
| apr_err = apr_get_home_directory (&homedir, username, pool); |
| if (apr_err) |
| return SVN_NO_ERROR; |
| |
| /* ### Any compelling reason to use svn's path lib here? */ |
| if (fname) |
| { |
| *path_p = apr_psprintf |
| (pool, "%s/%s/%s", homedir, SVN_CONFIG__USR_DIRECTORY, fname); |
| } |
| else |
| { |
| *path_p = apr_psprintf |
| (pool, "%s/%s", homedir, SVN_CONFIG__USR_DIRECTORY); |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| |
| /*** Exported interfaces. ***/ |
| |
| svn_error_t * |
| svn_config__parse_file (svn_config_t *cfg, const char *file, |
| svn_boolean_t must_exist) |
| { |
| svn_error_t *err = SVN_NO_ERROR; |
| parse_context_t ctx; |
| int ch, count; |
| /* "Why," you ask yourself, "is he using stdio FILE's instead of |
| apr_file_t's?" The answer is simple: newline translation. For |
| all that it has an APR_BINARY flag, APR doesn't do newline |
| translation in files. The only portable way I know to get |
| translated text files is to use the standard stdio library. */ |
| |
| FILE *fd = fopen (file, "rt"); |
| if (fd == NULL) |
| { |
| if (errno != ENOENT) |
| return svn_error_createf (SVN_ERR_BAD_FILENAME, |
| errno, NULL, cfg->pool, |
| "Can't open config file \"%s\"", file); |
| else if (must_exist && errno == ENOENT) |
| return svn_error_createf (SVN_ERR_BAD_FILENAME, |
| errno, NULL, cfg->pool, |
| "Can't find config file \"%s\"", file); |
| else |
| return SVN_NO_ERROR; |
| } |
| |
| ctx.cfg = cfg; |
| ctx.file = file; |
| ctx.fd = fd; |
| ctx.line = 1; |
| ctx.pool = svn_pool_create (cfg->pool); |
| ctx.section = svn_stringbuf_create("", ctx.pool); |
| ctx.option = svn_stringbuf_create("", ctx.pool); |
| ctx.value = svn_stringbuf_create("", ctx.pool); |
| |
| do |
| { |
| ch = skip_whitespace (fd, &count); |
| switch (ch) |
| { |
| case '[': /* Start of section header */ |
| if (count == 0) |
| err = parse_section_name (&ch, &ctx); |
| else |
| { |
| ch = EOF; |
| err = svn_error_createf (SVN_ERR_MALFORMED_FILE, |
| 0, NULL, ctx.pool, |
| "%s:%d: Section header" |
| " must start in the first column", |
| file, ctx.line); |
| } |
| break; |
| |
| case '#': /* Comment */ |
| if (count == 0) |
| ch = skip_to_eoln(fd); |
| else |
| { |
| ch = EOF; |
| err = svn_error_createf (SVN_ERR_MALFORMED_FILE, |
| 0, NULL, ctx.pool, |
| "%s:%d: Comment" |
| " must start in the first column", |
| file, ctx.line); |
| } |
| break; |
| |
| case '\n': /* Empty line */ |
| ++ctx.line; |
| break; |
| |
| case EOF: /* End of file or read error */ |
| break; |
| |
| default: |
| if (svn_stringbuf_isempty (ctx.section)) |
| { |
| ch = EOF; |
| err = svn_error_createf (SVN_ERR_MALFORMED_FILE, |
| 0, NULL, ctx.pool, |
| "%s:%d: Section header expected", |
| file, ctx.line); |
| } |
| else if (count != 0) |
| { |
| ch = EOF; |
| err = svn_error_createf (SVN_ERR_MALFORMED_FILE, |
| 0, NULL, ctx.pool, |
| "%s:%d: Option expected", |
| file, ctx.line); |
| } |
| else |
| err = parse_option (&ch, &ctx); |
| break; |
| } |
| } |
| while (ch != EOF); |
| |
| if (ferror (fd)) |
| { |
| err = svn_error_createf (-1, /* FIXME: Wrong error code. */ |
| errno, NULL, ctx.pool, |
| "%s:%d: Read error", file, ctx.line); |
| } |
| |
| svn_pool_destroy (ctx.pool); |
| fclose (fd); |
| return err; |
| } |
| |
| |
| svn_error_t * |
| svn_config_ensure (apr_pool_t *pool) |
| { |
| const char *path; |
| enum svn_node_kind kind; |
| apr_status_t apr_err; |
| svn_error_t *err; |
| |
| /* Ensure that the config directory exists. */ |
| SVN_ERR (svn_config__user_config_path (&path, NULL, pool)); |
| |
| if (! path) |
| return SVN_NO_ERROR; |
| |
| SVN_ERR (svn_io_check_path (path, &kind, pool)); |
| if (kind == svn_node_none) |
| { |
| apr_err = apr_dir_make (path, APR_OS_DEFAULT, pool); |
| if (! APR_STATUS_IS_SUCCESS (apr_err)) |
| return SVN_NO_ERROR; |
| } |
| else if (kind != svn_node_dir) |
| return SVN_NO_ERROR; |
| |
| /* Else, there's a configuration directory. */ |
| |
| /* If we get errors trying to do things below, just stop and return |
| success. There's no _need_ to init a config directory if |
| something's preventing it. */ |
| |
| /* Ensure that the `README' file exists. */ |
| SVN_ERR (svn_config__user_config_path |
| (&path, SVN_CONFIG__USR_README_FILE, pool)); |
| |
| if (! path) /* highly unlikely, since a previous call succeeded */ |
| return SVN_NO_ERROR; |
| |
| err = svn_io_check_path (path, &kind, pool); |
| if (err) |
| return SVN_NO_ERROR; |
| |
| if (kind == svn_node_none) |
| { |
| apr_file_t *f; |
| const char *contents = |
| "This directory holds run-time configuration information for Subversion\n" |
| "clients. The configuration files all share the same syntax, but you\n" |
| "should examine a particular file to learn what configuration\n" |
| "directives are valid for that file.\n" |
| "\n" |
| "The syntax is standard INI format:" |
| "\n" |
| "\n" |
| " - Empty lines, and lines starting with '#', are ignored.\n" |
| " The first significant line in a file must be a section header.\n" |
| "\n" |
| " - A section starts with a section header, which must start in\n" |
| " the first column:\n" |
| "\n" |
| " [section-name]\n" |
| "\n" |
| " - An option, which must always appear within a section, is a pair\n" |
| " (name, value). There are two valid forms for defining an\n" |
| " option, both of which must start in the first column:\n" |
| "\n" |
| " name: value\n" |
| " name = value\n" |
| "\n" |
| " Whitespace around the separator (:, =) is optional.\n" |
| "\n" |
| " - Section and option names are case-insensitive, but case is\n" |
| " preserved.\n" |
| "\n" |
| " - An option's value may be broken into several lines. The value\n" |
| " continuation lines must start with at least one whitespace.\n" |
| " Trailing whitespace in the previous line, the newline character\n" |
| " and the leading whitespace in the continuation line is compressed\n" |
| " into a single space character.\n" |
| "\n" |
| " - All leading and trailing whitespace around a value is trimmed,\n" |
| " but the whitespace within a value is preserved, with the\n" |
| " exception of whitespace around line continuations, as\n" |
| " described above.\n" |
| "\n" |
| " - When a value is a list, it is comma-separated. Again, the\n" |
| " whitespace around each element of the list is trimmed.\n" |
| "\n" |
| #if 0 /* expansion not implemented yet */ |
| " - Option values may be expanded within a value by enclosing the\n" |
| " option name in parentheses, preceded by a percent sign:\n" |
| "\n" |
| " %(name)\n" |
| "\n" |
| " The expansion is performed recursively and on demand, during\n" |
| " svn_option_get. The name is first searched for in the same\n" |
| " section, then in the special [DEFAULTS] section. If the name\n" |
| " is not found, the whole %(name) placeholder is left\n" |
| " unchanged.\n" |
| "\n" |
| " Any modifications to the configuration data invalidate all\n" |
| " previously expanded values, so that the next svn_option_get\n" |
| " will take the modifications into account.\n" |
| "\n" |
| #endif /* 0 */ |
| "\n" |
| "Configuration data in the Windows registry\n" |
| "==========================================\n" |
| "\n" |
| "On Windows, configuration data may also be stored in the registry. The\n" |
| "functions svn_config_read and svn_config_merge will read from the\n" |
| "registry when passed file names of the form:\n" |
| "\n" |
| " REGISTRY:<hive>/path/to/config-key\n" |
| "\n" |
| "The REGISTRY: prefix must be in upper case. The <hive> part must be\n" |
| "one of:\n" |
| "\n" |
| " HKLM for HKEY_LOCAL_MACHINE\n" |
| " HKCU for HKEY_CURRENT_USER\n" |
| "\n" |
| "The values in config-key represent the options in the [DEFAULTS] section." |
| "\n" |
| "The keys below config-key represent other sections, and their values\n" |
| "represent the options. Only values of type REG_SZ will be used; other\n" |
| "values, as well as the keys' default values, will be ignored.\n" |
| "\n" |
| "\n" |
| "File locations\n" |
| "==============\n" |
| "\n" |
| "Typically, Subversion uses two config directories, one for site-wide\n" |
| "configuration,\n" |
| "\n" |
| " /etc/subversion/proxies\n" |
| " /etc/subversion/config\n" |
| " /etc/subversion/hairstyles\n" |
| " -- or --\n" |
| " REGISTRY:HKLM\\Software\\Tigris.org\\Subversion\\Proxies\n" |
| " REGISTRY:HKLM\\Software\\Tigris.org\\Subversion\\Config\n" |
| " REGISTRY:HKLM\\Software\\Tigris.org\\Subversion\\Hairstyles\n" |
| "\n" |
| "and one for per-user configuration:\n" |
| "\n" |
| " ~/.subversion/proxies\n" |
| " ~/.subversion/config\n" |
| " ~/.subversion/hairstyles\n" |
| " -- or --\n" |
| " REGISTRY:HKCU\\Software\\Tigris.org\\Subversion\\Proxies\n" |
| " REGISTRY:HKCU\\Software\\Tigris.org\\Subversion\\Config\n" |
| " REGISTRY:HKCU\\Software\\Tigris.org\\Subversion\\Hairstyles\n" |
| "\n"; |
| |
| apr_err = apr_file_open (&f, path, |
| (APR_WRITE | APR_CREATE | APR_EXCL), |
| APR_OS_DEFAULT, |
| pool); |
| |
| if (APR_STATUS_IS_SUCCESS (apr_err)) |
| { |
| apr_err = apr_file_write_full (f, contents, strlen (contents), NULL); |
| if (apr_err) |
| return svn_error_createf (apr_err, 0, NULL, pool, |
| "writing config file `%s'", path); |
| |
| apr_err = apr_file_close (f); |
| if (apr_err) |
| return svn_error_createf (apr_err, 0, NULL, pool, |
| "closing config file `%s'", path); |
| } |
| } |
| |
| /* Ensure that the `proxies' file exists. */ |
| SVN_ERR (svn_config__user_config_path |
| (&path, SVN_CONFIG__USR_PROXY_FILE, pool)); |
| |
| if (! path) /* highly unlikely, since a previous call succeeded */ |
| return SVN_NO_ERROR; |
| |
| err = svn_io_check_path (path, &kind, pool); |
| if (err) |
| return SVN_NO_ERROR; |
| |
| if (kind == svn_node_none) |
| { |
| apr_file_t *f; |
| const char *contents = |
| "### This file determines which proxy servers to use, if\n" |
| "### any, when contacting a remote repository.\n" |
| "###\n" |
| "### The commented-out examples below are intended only to\n" |
| "### demonstrate how to use this file; any resemblance to\n" |
| "### actual servers, living or dead, is entirely\n" |
| "### coincidental.\n" |
| "\n" |
| "### In this section, the URL of the repository you're\n" |
| "### trying to access is matched against the patterns on\n" |
| "### the right. If a match is found, the proxy info is\n" |
| "### taken from the section with the corresponding name.\n" |
| "# [groups]\n" |
| "# group1 = *.collab.net\n" |
| "# othergroup = repository.blarggitywhoomph.com\n" |
| "\n" |
| "### Information for the first group:\n" |
| "# [group1]\n" |
| "# host = proxy1.some-domain-name.com\n" |
| "# port = 80\n" |
| "# username = blah\n" |
| "# password = doubleblah\n" |
| "\n" |
| "### Information for the second group:\n" |
| "# [othergroup]\n" |
| "# host = proxy2.some-domain-name.com\n" |
| "# port = 9000\n" |
| "# No username and password, so use the defaults below.\n" |
| "\n" |
| "### If there is a `default' section, then anything not set\n" |
| "### by a specifically matched group is taken from the\n" |
| "### defaults. Thus, if you go through the same proxy\n" |
| "### server to reach every site on the Internet, you\n" |
| "### probably just want to put that server's information in\n" |
| "### the `default' section and not bother with `groups' or\n" |
| "### any other sections.\n" |
| "### \n" |
| "### If you go through a proxy for all but a few sites, you can\n" |
| "### list those exceptions under `no_proxy', see below. This only\n" |
| "### overrides defaults, not explicitly matched proxies.\n" |
| "# [default]\n" |
| "# no_proxy = *.exception.com, www.internal-site.org\n" |
| "# host = defaultproxy.whatever.com\n" |
| "# port = 7000\n" |
| "# username = defaultusername\n" |
| "# password = defaultpassword\n"; |
| |
| apr_err = apr_file_open (&f, path, |
| (APR_WRITE | APR_CREATE | APR_EXCL), |
| APR_OS_DEFAULT, |
| pool); |
| |
| if (APR_STATUS_IS_SUCCESS (apr_err)) |
| { |
| apr_err = apr_file_write_full (f, contents, strlen (contents), NULL); |
| if (apr_err) |
| return svn_error_createf (apr_err, 0, NULL, pool, |
| "writing config file `%s'", path); |
| |
| apr_err = apr_file_close (f); |
| if (apr_err) |
| return svn_error_createf (apr_err, 0, NULL, pool, |
| "closing config file `%s'", path); |
| } |
| } |
| |
| return SVN_NO_ERROR; |
| } |
| |
| |
| |
| /* |
| * local variables: |
| * eval: (load-file "../../tools/dev/svn-dev.el") |
| * end: |
| */ |