summaryrefslogtreecommitdiffstats
path: root/subversion/libsvn_subr/subst.c
diff options
context:
space:
mode:
Diffstat (limited to 'subversion/libsvn_subr/subst.c')
-rw-r--r--subversion/libsvn_subr/subst.c2025
1 files changed, 2025 insertions, 0 deletions
diff --git a/subversion/libsvn_subr/subst.c b/subversion/libsvn_subr/subst.c
new file mode 100644
index 0000000..f69dcf8
--- /dev/null
+++ b/subversion/libsvn_subr/subst.c
@@ -0,0 +1,2025 @@
+/*
+ * subst.c : generic eol/keyword substitution routines
+ *
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ */
+
+
+
+#define APR_WANT_STRFUNC
+#include <apr_want.h>
+
+#include <stdlib.h>
+#include <assert.h>
+#include <apr_pools.h>
+#include <apr_tables.h>
+#include <apr_file_io.h>
+#include <apr_strings.h>
+
+#include "svn_hash.h"
+#include "svn_cmdline.h"
+#include "svn_types.h"
+#include "svn_string.h"
+#include "svn_time.h"
+#include "svn_dirent_uri.h"
+#include "svn_path.h"
+#include "svn_error.h"
+#include "svn_utf.h"
+#include "svn_io.h"
+#include "svn_subst.h"
+#include "svn_pools.h"
+#include "private/svn_io_private.h"
+
+#include "svn_private_config.h"
+
+#include "private/svn_string_private.h"
+
+/**
+ * The textual elements of a detranslated special file. One of these
+ * strings must appear as the first element of any special file as it
+ * exists in the repository or the text base.
+ */
+#define SVN_SUBST__SPECIAL_LINK_STR "link"
+
+void
+svn_subst_eol_style_from_value(svn_subst_eol_style_t *style,
+ const char **eol,
+ const char *value)
+{
+ if (value == NULL)
+ {
+ /* property doesn't exist. */
+ *eol = NULL;
+ if (style)
+ *style = svn_subst_eol_style_none;
+ }
+ else if (! strcmp("native", value))
+ {
+ *eol = APR_EOL_STR; /* whee, a portability library! */
+ if (style)
+ *style = svn_subst_eol_style_native;
+ }
+ else if (! strcmp("LF", value))
+ {
+ *eol = "\n";
+ if (style)
+ *style = svn_subst_eol_style_fixed;
+ }
+ else if (! strcmp("CR", value))
+ {
+ *eol = "\r";
+ if (style)
+ *style = svn_subst_eol_style_fixed;
+ }
+ else if (! strcmp("CRLF", value))
+ {
+ *eol = "\r\n";
+ if (style)
+ *style = svn_subst_eol_style_fixed;
+ }
+ else
+ {
+ *eol = NULL;
+ if (style)
+ *style = svn_subst_eol_style_unknown;
+ }
+}
+
+
+svn_boolean_t
+svn_subst_translation_required(svn_subst_eol_style_t style,
+ const char *eol,
+ apr_hash_t *keywords,
+ svn_boolean_t special,
+ svn_boolean_t force_eol_check)
+{
+ return (special || keywords
+ || (style != svn_subst_eol_style_none && force_eol_check)
+ || (style == svn_subst_eol_style_native &&
+ strcmp(APR_EOL_STR, SVN_SUBST_NATIVE_EOL_STR) != 0)
+ || (style == svn_subst_eol_style_fixed &&
+ strcmp(APR_EOL_STR, eol) != 0));
+}
+
+
+
+/* Helper function for svn_subst_build_keywords */
+
+/* Given a printf-like format string, return a string with proper
+ * information filled in.
+ *
+ * Important API note: This function is the core of the implementation of
+ * svn_subst_build_keywords (all versions), and as such must implement the
+ * tolerance of NULL and zero inputs that that function's documention
+ * stipulates.
+ *
+ * The format codes:
+ *
+ * %a author of this revision
+ * %b basename of the URL of this file
+ * %d short format of date of this revision
+ * %D long format of date of this revision
+ * %P path relative to root of repos
+ * %r number of this revision
+ * %R root url of repository
+ * %u URL of this file
+ * %_ a space
+ * %% a literal %
+ *
+ * The following special format codes are also recognized:
+ * %H is equivalent to %P%_%r%_%d%_%a
+ * %I is equivalent to %b%_%r%_%d%_%a
+ *
+ * All memory is allocated out of @a pool.
+ */
+static svn_string_t *
+keyword_printf(const char *fmt,
+ const char *rev,
+ const char *url,
+ const char *repos_root_url,
+ apr_time_t date,
+ const char *author,
+ apr_pool_t *pool)
+{
+ svn_stringbuf_t *value = svn_stringbuf_ncreate("", 0, pool);
+ const char *cur;
+ size_t n;
+
+ for (;;)
+ {
+ cur = fmt;
+
+ while (*cur != '\0' && *cur != '%')
+ cur++;
+
+ if ((n = cur - fmt) > 0) /* Do we have an as-is string? */
+ svn_stringbuf_appendbytes(value, fmt, n);
+
+ if (*cur == '\0')
+ break;
+
+ switch (cur[1])
+ {
+ case 'a': /* author of this revision */
+ if (author)
+ svn_stringbuf_appendcstr(value, author);
+ break;
+ case 'b': /* basename of this file */
+ if (url && *url)
+ {
+ const char *base_name = svn_uri_basename(url, pool);
+ svn_stringbuf_appendcstr(value, base_name);
+ }
+ break;
+ case 'd': /* short format of date of this revision */
+ if (date)
+ {
+ apr_time_exp_t exploded_time;
+ const char *human;
+
+ apr_time_exp_gmt(&exploded_time, date);
+
+ human = apr_psprintf(pool, "%04d-%02d-%02d %02d:%02d:%02dZ",
+ exploded_time.tm_year + 1900,
+ exploded_time.tm_mon + 1,
+ exploded_time.tm_mday,
+ exploded_time.tm_hour,
+ exploded_time.tm_min,
+ exploded_time.tm_sec);
+
+ svn_stringbuf_appendcstr(value, human);
+ }
+ break;
+ case 'D': /* long format of date of this revision */
+ if (date)
+ svn_stringbuf_appendcstr(value,
+ svn_time_to_human_cstring(date, pool));
+ break;
+ case 'P': /* relative path of this file */
+ if (repos_root_url && *repos_root_url != '\0' && url && *url != '\0')
+ {
+ const char *repos_relpath;
+
+ repos_relpath = svn_uri_skip_ancestor(repos_root_url, url, pool);
+ if (repos_relpath)
+ svn_stringbuf_appendcstr(value, repos_relpath);
+ }
+ break;
+ case 'R': /* root of repos */
+ if (repos_root_url && *repos_root_url != '\0')
+ svn_stringbuf_appendcstr(value, repos_root_url);
+ break;
+ case 'r': /* number of this revision */
+ if (rev)
+ svn_stringbuf_appendcstr(value, rev);
+ break;
+ case 'u': /* URL of this file */
+ if (url)
+ svn_stringbuf_appendcstr(value, url);
+ break;
+ case '_': /* '%_' => a space */
+ svn_stringbuf_appendbyte(value, ' ');
+ break;
+ case '%': /* '%%' => a literal % */
+ svn_stringbuf_appendbyte(value, *cur);
+ break;
+ case '\0': /* '%' as the last character of the string. */
+ svn_stringbuf_appendbyte(value, *cur);
+ /* Now go back one character, since this was just a one character
+ * sequence, whereas all others are two characters, and we do not
+ * want to skip the null terminator entirely and carry on
+ * formatting random memory contents. */
+ cur--;
+ break;
+ case 'H':
+ {
+ svn_string_t *s = keyword_printf("%P%_%r%_%d%_%a", rev, url,
+ repos_root_url, date, author,
+ pool);
+ svn_stringbuf_appendcstr(value, s->data);
+ }
+ break;
+ case 'I':
+ {
+ svn_string_t *s = keyword_printf("%b%_%r%_%d%_%a", rev, url,
+ repos_root_url, date, author,
+ pool);
+ svn_stringbuf_appendcstr(value, s->data);
+ }
+ break;
+ default: /* Unrecognized code, just print it literally. */
+ svn_stringbuf_appendbytes(value, cur, 2);
+ break;
+ }
+
+ /* Format code is processed - skip it, and get ready for next chunk. */
+ fmt = cur + 2;
+ }
+
+ return svn_stringbuf__morph_into_string(value);
+}
+
+static svn_error_t *
+build_keywords(apr_hash_t **kw,
+ svn_boolean_t expand_custom_keywords,
+ const char *keywords_val,
+ const char *rev,
+ const char *url,
+ const char *repos_root_url,
+ apr_time_t date,
+ const char *author,
+ apr_pool_t *pool)
+{
+ apr_array_header_t *keyword_tokens;
+ int i;
+ *kw = apr_hash_make(pool);
+
+ keyword_tokens = svn_cstring_split(keywords_val, " \t\v\n\b\r\f",
+ TRUE /* chop */, pool);
+
+ for (i = 0; i < keyword_tokens->nelts; ++i)
+ {
+ const char *keyword = APR_ARRAY_IDX(keyword_tokens, i, const char *);
+ const char *custom_fmt = NULL;
+
+ if (expand_custom_keywords)
+ {
+ char *sep;
+
+ /* Check if there is a custom keyword definition, started by '='. */
+ sep = strchr(keyword, '=');
+ if (sep)
+ {
+ *sep = '\0'; /* Split keyword's name from custom format. */
+ custom_fmt = sep + 1;
+ }
+ }
+
+ if (custom_fmt)
+ {
+ svn_string_t *custom_val;
+
+ /* Custom keywords must be allowed to match the name of an
+ * existing fixed keyword. This is for compatibility purposes,
+ * in case new fixed keywords are added to Subversion which
+ * happen to match a custom keyword defined somewhere.
+ * There is only one global namespace for keyword names. */
+ custom_val = keyword_printf(custom_fmt, rev, url, repos_root_url,
+ date, author, pool);
+ svn_hash_sets(*kw, keyword, custom_val);
+ }
+ else if ((! strcmp(keyword, SVN_KEYWORD_REVISION_LONG))
+ || (! strcmp(keyword, SVN_KEYWORD_REVISION_MEDIUM))
+ || (! svn_cstring_casecmp(keyword, SVN_KEYWORD_REVISION_SHORT)))
+ {
+ svn_string_t *revision_val;
+
+ revision_val = keyword_printf("%r", rev, url, repos_root_url,
+ date, author, pool);
+ svn_hash_sets(*kw, SVN_KEYWORD_REVISION_LONG, revision_val);
+ svn_hash_sets(*kw, SVN_KEYWORD_REVISION_MEDIUM, revision_val);
+ svn_hash_sets(*kw, SVN_KEYWORD_REVISION_SHORT, revision_val);
+ }
+ else if ((! strcmp(keyword, SVN_KEYWORD_DATE_LONG))
+ || (! svn_cstring_casecmp(keyword, SVN_KEYWORD_DATE_SHORT)))
+ {
+ svn_string_t *date_val;
+
+ date_val = keyword_printf("%D", rev, url, repos_root_url, date,
+ author, pool);
+ svn_hash_sets(*kw, SVN_KEYWORD_DATE_LONG, date_val);
+ svn_hash_sets(*kw, SVN_KEYWORD_DATE_SHORT, date_val);
+ }
+ else if ((! strcmp(keyword, SVN_KEYWORD_AUTHOR_LONG))
+ || (! svn_cstring_casecmp(keyword, SVN_KEYWORD_AUTHOR_SHORT)))
+ {
+ svn_string_t *author_val;
+
+ author_val = keyword_printf("%a", rev, url, repos_root_url, date,
+ author, pool);
+ svn_hash_sets(*kw, SVN_KEYWORD_AUTHOR_LONG, author_val);
+ svn_hash_sets(*kw, SVN_KEYWORD_AUTHOR_SHORT, author_val);
+ }
+ else if ((! strcmp(keyword, SVN_KEYWORD_URL_LONG))
+ || (! svn_cstring_casecmp(keyword, SVN_KEYWORD_URL_SHORT)))
+ {
+ svn_string_t *url_val;
+
+ url_val = keyword_printf("%u", rev, url, repos_root_url, date,
+ author, pool);
+ svn_hash_sets(*kw, SVN_KEYWORD_URL_LONG, url_val);
+ svn_hash_sets(*kw, SVN_KEYWORD_URL_SHORT, url_val);
+ }
+ else if ((! svn_cstring_casecmp(keyword, SVN_KEYWORD_ID)))
+ {
+ svn_string_t *id_val;
+
+ id_val = keyword_printf("%b %r %d %a", rev, url, repos_root_url,
+ date, author, pool);
+ svn_hash_sets(*kw, SVN_KEYWORD_ID, id_val);
+ }
+ else if ((! svn_cstring_casecmp(keyword, SVN_KEYWORD_HEADER)))
+ {
+ svn_string_t *header_val;
+
+ header_val = keyword_printf("%u %r %d %a", rev, url, repos_root_url,
+ date, author, pool);
+ svn_hash_sets(*kw, SVN_KEYWORD_HEADER, header_val);
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_subst_build_keywords2(apr_hash_t **kw,
+ const char *keywords_val,
+ const char *rev,
+ const char *url,
+ apr_time_t date,
+ const char *author,
+ apr_pool_t *pool)
+{
+ return svn_error_trace(build_keywords(kw, FALSE, keywords_val, rev, url,
+ NULL, date, author, pool));
+}
+
+
+svn_error_t *
+svn_subst_build_keywords3(apr_hash_t **kw,
+ const char *keywords_val,
+ const char *rev,
+ const char *url,
+ const char *repos_root_url,
+ apr_time_t date,
+ const char *author,
+ apr_pool_t *pool)
+{
+ return svn_error_trace(build_keywords(kw, TRUE, keywords_val,
+ rev, url, repos_root_url,
+ date, author, pool));
+}
+
+
+/*** Helpers for svn_subst_translate_stream2 ***/
+
+
+/* Write out LEN bytes of BUF into STREAM. */
+/* ### TODO: 'stream_write()' would be a better name for this. */
+static svn_error_t *
+translate_write(svn_stream_t *stream,
+ const void *buf,
+ apr_size_t len)
+{
+ SVN_ERR(svn_stream_write(stream, buf, &len));
+ /* (No need to check LEN, as a short write always produces an error.) */
+ return SVN_NO_ERROR;
+}
+
+
+/* Perform the substitution of VALUE into keyword string BUF (with len
+ *LEN), given a pre-parsed KEYWORD (and KEYWORD_LEN), and updating
+ *LEN to the new size of the substituted result. Return TRUE if all
+ goes well, FALSE otherwise. If VALUE is NULL, keyword will be
+ contracted, else it will be expanded. */
+static svn_boolean_t
+translate_keyword_subst(char *buf,
+ apr_size_t *len,
+ const char *keyword,
+ apr_size_t keyword_len,
+ const svn_string_t *value)
+{
+ char *buf_ptr;
+
+ /* Make sure we gotz good stuffs. */
+ assert(*len <= SVN_KEYWORD_MAX_LEN);
+ assert((buf[0] == '$') && (buf[*len - 1] == '$'));
+
+ /* Need at least a keyword and two $'s. */
+ if (*len < keyword_len + 2)
+ return FALSE;
+
+ /* Need at least space for two $'s, two spaces and a colon, and that
+ leaves zero space for the value itself. */
+ if (keyword_len > SVN_KEYWORD_MAX_LEN - 5)
+ return FALSE;
+
+ /* The keyword needs to match what we're looking for. */
+ if (strncmp(buf + 1, keyword, keyword_len))
+ return FALSE;
+
+ buf_ptr = buf + 1 + keyword_len;
+
+ /* Check for fixed-length expansion.
+ * The format of fixed length keyword and its data is
+ * Unexpanded keyword: "$keyword:: $"
+ * Expanded keyword: "$keyword:: value $"
+ * Expanded kw with filling: "$keyword:: value $"
+ * Truncated keyword: "$keyword:: longval#$"
+ */
+ if ((buf_ptr[0] == ':') /* first char after keyword is ':' */
+ && (buf_ptr[1] == ':') /* second char after keyword is ':' */
+ && (buf_ptr[2] == ' ') /* third char after keyword is ' ' */
+ && ((buf[*len - 2] == ' ') /* has ' ' for next to last character */
+ || (buf[*len - 2] == '#')) /* .. or has '#' for next to last
+ character */
+ && ((6 + keyword_len) < *len)) /* holds "$kw:: x $" at least */
+ {
+ /* This is fixed length keyword, so *len remains unchanged */
+ apr_size_t max_value_len = *len - (6 + keyword_len);
+
+ if (! value)
+ {
+ /* no value, so unexpand */
+ buf_ptr += 2;
+ while (*buf_ptr != '$')
+ *(buf_ptr++) = ' ';
+ }
+ else
+ {
+ if (value->len <= max_value_len)
+ { /* replacement not as long as template, pad with spaces */
+ strncpy(buf_ptr + 3, value->data, value->len);
+ buf_ptr += 3 + value->len;
+ while (*buf_ptr != '$')
+ *(buf_ptr++) = ' ';
+ }
+ else
+ {
+ /* replacement needs truncating */
+ strncpy(buf_ptr + 3, value->data, max_value_len);
+ buf[*len - 2] = '#';
+ buf[*len - 1] = '$';
+ }
+ }
+ return TRUE;
+ }
+
+ /* Check for unexpanded keyword. */
+ else if (buf_ptr[0] == '$') /* "$keyword$" */
+ {
+ /* unexpanded... */
+ if (value)
+ {
+ /* ...so expand. */
+ buf_ptr[0] = ':';
+ buf_ptr[1] = ' ';
+ if (value->len)
+ {
+ apr_size_t vallen = value->len;
+
+ /* "$keyword: value $" */
+ if (vallen > (SVN_KEYWORD_MAX_LEN - 5 - keyword_len))
+ vallen = SVN_KEYWORD_MAX_LEN - 5 - keyword_len;
+ strncpy(buf_ptr + 2, value->data, vallen);
+ buf_ptr[2 + vallen] = ' ';
+ buf_ptr[2 + vallen + 1] = '$';
+ *len = 5 + keyword_len + vallen;
+ }
+ else
+ {
+ /* "$keyword: $" */
+ buf_ptr[2] = '$';
+ *len = 4 + keyword_len;
+ }
+ }
+ else
+ {
+ /* ...but do nothing. */
+ }
+ return TRUE;
+ }
+
+ /* Check for expanded keyword. */
+ else if (((*len >= 4 + keyword_len ) /* holds at least "$keyword: $" */
+ && (buf_ptr[0] == ':') /* first char after keyword is ':' */
+ && (buf_ptr[1] == ' ') /* second char after keyword is ' ' */
+ && (buf[*len - 2] == ' '))
+ || ((*len >= 3 + keyword_len ) /* holds at least "$keyword:$" */
+ && (buf_ptr[0] == ':') /* first char after keyword is ':' */
+ && (buf_ptr[1] == '$'))) /* second char after keyword is '$' */
+ {
+ /* expanded... */
+ if (! value)
+ {
+ /* ...so unexpand. */
+ buf_ptr[0] = '$';
+ *len = 2 + keyword_len;
+ }
+ else
+ {
+ /* ...so re-expand. */
+ buf_ptr[0] = ':';
+ buf_ptr[1] = ' ';
+ if (value->len)
+ {
+ apr_size_t vallen = value->len;
+
+ /* "$keyword: value $" */
+ if (vallen > (SVN_KEYWORD_MAX_LEN - 5 - keyword_len))
+ vallen = SVN_KEYWORD_MAX_LEN - 5 - keyword_len;
+ strncpy(buf_ptr + 2, value->data, vallen);
+ buf_ptr[2 + vallen] = ' ';
+ buf_ptr[2 + vallen + 1] = '$';
+ *len = 5 + keyword_len + vallen;
+ }
+ else
+ {
+ /* "$keyword: $" */
+ buf_ptr[2] = '$';
+ *len = 4 + keyword_len;
+ }
+ }
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/* Parse BUF (whose length is LEN, and which starts and ends with '$'),
+ trying to match one of the keyword names in KEYWORDS. If such a
+ keyword is found, update *KEYWORD_NAME with the keyword name and
+ return TRUE. */
+static svn_boolean_t
+match_keyword(char *buf,
+ apr_size_t len,
+ char *keyword_name,
+ apr_hash_t *keywords)
+{
+ apr_size_t i;
+
+ /* Early return for ignored keywords */
+ if (! keywords)
+ return FALSE;
+
+ /* Extract the name of the keyword */
+ for (i = 0; i < len - 2 && buf[i + 1] != ':'; i++)
+ keyword_name[i] = buf[i + 1];
+ keyword_name[i] = '\0';
+
+ return svn_hash_gets(keywords, keyword_name) != NULL;
+}
+
+/* Try to translate keyword *KEYWORD_NAME in BUF (whose length is LEN):
+ optionally perform the substitution in place, update *LEN with
+ the new length of the translated keyword string, and return TRUE.
+ If this buffer doesn't contain a known keyword pattern, leave BUF
+ and *LEN untouched and return FALSE.
+
+ See the docstring for svn_subst_copy_and_translate for how the
+ EXPAND and KEYWORDS parameters work.
+
+ NOTE: It is assumed that BUF has been allocated to be at least
+ SVN_KEYWORD_MAX_LEN bytes longs, and that the data in BUF is less
+ than or equal SVN_KEYWORD_MAX_LEN in length. Also, any expansions
+ which would result in a keyword string which is greater than
+ SVN_KEYWORD_MAX_LEN will have their values truncated in such a way
+ that the resultant keyword string is still valid (begins with
+ "$Keyword:", ends in " $" and is SVN_KEYWORD_MAX_LEN bytes long). */
+static svn_boolean_t
+translate_keyword(char *buf,
+ apr_size_t *len,
+ const char *keyword_name,
+ svn_boolean_t expand,
+ apr_hash_t *keywords)
+{
+ const svn_string_t *value;
+
+ /* Make sure we gotz good stuffs. */
+ assert(*len <= SVN_KEYWORD_MAX_LEN);
+ assert((buf[0] == '$') && (buf[*len - 1] == '$'));
+
+ /* Early return for ignored keywords */
+ if (! keywords)
+ return FALSE;
+
+ value = svn_hash_gets(keywords, keyword_name);
+
+ if (value)
+ {
+ return translate_keyword_subst(buf, len,
+ keyword_name, strlen(keyword_name),
+ expand ? value : NULL);
+ }
+
+ return FALSE;
+}
+
+/* A boolean expression that evaluates to true if the first STR_LEN characters
+ of the string STR are one of the end-of-line strings LF, CR, or CRLF;
+ to false otherwise. */
+#define STRING_IS_EOL(str, str_len) \
+ (((str_len) == 2 && (str)[0] == '\r' && (str)[1] == '\n') || \
+ ((str_len) == 1 && ((str)[0] == '\n' || (str)[0] == '\r')))
+
+/* A boolean expression that evaluates to true if the end-of-line string EOL1,
+ having length EOL1_LEN, and the end-of-line string EOL2, having length
+ EOL2_LEN, are different, assuming that EOL1 and EOL2 are both from the
+ set {"\n", "\r", "\r\n"}; to false otherwise.
+
+ Given that EOL1 and EOL2 are either "\n", "\r", or "\r\n", then if
+ EOL1_LEN is not the same as EOL2_LEN, then EOL1 and EOL2 are of course
+ different. If EOL1_LEN and EOL2_LEN are both 2 then EOL1 and EOL2 are both
+ "\r\n" and *EOL1 == *EOL2. Otherwise, EOL1_LEN and EOL2_LEN are both 1.
+ We need only check the one character for equality to determine whether
+ EOL1 and EOL2 are different in that case. */
+#define DIFFERENT_EOL_STRINGS(eol1, eol1_len, eol2, eol2_len) \
+ (((eol1_len) != (eol2_len)) || (*(eol1) != *(eol2)))
+
+
+/* Translate the newline string NEWLINE_BUF (of length NEWLINE_LEN) to
+ the newline string EOL_STR (of length EOL_STR_LEN), writing the
+ result (which is always EOL_STR) to the stream DST.
+
+ This function assumes that NEWLINE_BUF is either "\n", "\r", or "\r\n".
+
+ Also check for consistency of the source newline strings across
+ multiple calls, using SRC_FORMAT (length *SRC_FORMAT_LEN) as a cache
+ of the first newline found. If the current newline is not the same
+ as SRC_FORMAT, look to the REPAIR parameter. If REPAIR is TRUE,
+ ignore the inconsistency, else return an SVN_ERR_IO_INCONSISTENT_EOL
+ error. If *SRC_FORMAT_LEN is 0, assume we are examining the first
+ newline in the file, and copy it to {SRC_FORMAT, *SRC_FORMAT_LEN} to
+ use for later consistency checks.
+
+ If TRANSLATED_EOL is not NULL, then set *TRANSLATED_EOL to TRUE if the
+ newline string that was written (EOL_STR) is not the same as the newline
+ string that was translated (NEWLINE_BUF), otherwise leave *TRANSLATED_EOL
+ untouched.
+
+ Note: all parameters are required even if REPAIR is TRUE.
+ ### We could require that REPAIR must not change across a sequence of
+ calls, and could then optimize by not using SRC_FORMAT at all if
+ REPAIR is TRUE.
+*/
+static svn_error_t *
+translate_newline(const char *eol_str,
+ apr_size_t eol_str_len,
+ char *src_format,
+ apr_size_t *src_format_len,
+ const char *newline_buf,
+ apr_size_t newline_len,
+ svn_stream_t *dst,
+ svn_boolean_t *translated_eol,
+ svn_boolean_t repair)
+{
+ SVN_ERR_ASSERT(STRING_IS_EOL(newline_buf, newline_len));
+
+ /* If we've seen a newline before, compare it with our cache to
+ check for consistency, else cache it for future comparisons. */
+ if (*src_format_len)
+ {
+ /* Comparing with cache. If we are inconsistent and
+ we are NOT repairing the file, generate an error! */
+ if ((! repair) && DIFFERENT_EOL_STRINGS(src_format, *src_format_len,
+ newline_buf, newline_len))
+ return svn_error_create(SVN_ERR_IO_INCONSISTENT_EOL, NULL, NULL);
+ }
+ else
+ {
+ /* This is our first line ending, so cache it before
+ handling it. */
+ strncpy(src_format, newline_buf, newline_len);
+ *src_format_len = newline_len;
+ }
+
+ /* Write the desired newline */
+ SVN_ERR(translate_write(dst, eol_str, eol_str_len));
+
+ /* Report whether we translated it. Note: Not using DIFFERENT_EOL_STRINGS()
+ * because EOL_STR may not be a valid EOL sequence. */
+ if (translated_eol != NULL &&
+ (eol_str_len != newline_len ||
+ memcmp(eol_str, newline_buf, eol_str_len) != 0))
+ *translated_eol = TRUE;
+
+ return SVN_NO_ERROR;
+}
+
+
+
+/*** Public interfaces. ***/
+
+svn_boolean_t
+svn_subst_keywords_differ(const svn_subst_keywords_t *a,
+ const svn_subst_keywords_t *b,
+ svn_boolean_t compare_values)
+{
+ if (((a == NULL) && (b == NULL)) /* no A or B */
+ /* no A, and B has no contents */
+ || ((a == NULL)
+ && (b->revision == NULL)
+ && (b->date == NULL)
+ && (b->author == NULL)
+ && (b->url == NULL))
+ /* no B, and A has no contents */
+ || ((b == NULL) && (a->revision == NULL)
+ && (a->date == NULL)
+ && (a->author == NULL)
+ && (a->url == NULL))
+ /* neither A nor B has any contents */
+ || ((a != NULL) && (b != NULL)
+ && (b->revision == NULL)
+ && (b->date == NULL)
+ && (b->author == NULL)
+ && (b->url == NULL)
+ && (a->revision == NULL)
+ && (a->date == NULL)
+ && (a->author == NULL)
+ && (a->url == NULL)))
+ {
+ return FALSE;
+ }
+ else if ((a == NULL) || (b == NULL))
+ return TRUE;
+
+ /* Else both A and B have some keywords. */
+
+ if ((! a->revision) != (! b->revision))
+ return TRUE;
+ else if ((compare_values && (a->revision != NULL))
+ && (strcmp(a->revision->data, b->revision->data) != 0))
+ return TRUE;
+
+ if ((! a->date) != (! b->date))
+ return TRUE;
+ else if ((compare_values && (a->date != NULL))
+ && (strcmp(a->date->data, b->date->data) != 0))
+ return TRUE;
+
+ if ((! a->author) != (! b->author))
+ return TRUE;
+ else if ((compare_values && (a->author != NULL))
+ && (strcmp(a->author->data, b->author->data) != 0))
+ return TRUE;
+
+ if ((! a->url) != (! b->url))
+ return TRUE;
+ else if ((compare_values && (a->url != NULL))
+ && (strcmp(a->url->data, b->url->data) != 0))
+ return TRUE;
+
+ /* Else we never found a difference, so they must be the same. */
+
+ return FALSE;
+}
+
+svn_boolean_t
+svn_subst_keywords_differ2(apr_hash_t *a,
+ apr_hash_t *b,
+ svn_boolean_t compare_values,
+ apr_pool_t *pool)
+{
+ apr_hash_index_t *hi;
+ unsigned int a_count, b_count;
+
+ /* An empty hash is logically equal to a NULL,
+ * as far as this API is concerned. */
+ a_count = (a == NULL) ? 0 : apr_hash_count(a);
+ b_count = (b == NULL) ? 0 : apr_hash_count(b);
+
+ if (a_count != b_count)
+ return TRUE;
+
+ if (a_count == 0)
+ return FALSE;
+
+ /* The hashes are both non-NULL, and have the same number of items.
+ * We must check that every item of A is present in B. */
+ for (hi = apr_hash_first(pool, a); hi; hi = apr_hash_next(hi))
+ {
+ const void *key;
+ apr_ssize_t klen;
+ void *void_a_val;
+ svn_string_t *a_val, *b_val;
+
+ apr_hash_this(hi, &key, &klen, &void_a_val);
+ a_val = void_a_val;
+ b_val = apr_hash_get(b, key, klen);
+
+ if (!b_val || (compare_values && !svn_string_compare(a_val, b_val)))
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+
+/* Baton for translate_chunk() to store its state in. */
+struct translation_baton
+{
+ const char *eol_str;
+ svn_boolean_t *translated_eol;
+ svn_boolean_t repair;
+ apr_hash_t *keywords;
+ svn_boolean_t expand;
+
+ /* 'short boolean' array that encodes what character values
+ may trigger a translation action, hence are 'interesting' */
+ char interesting[256];
+
+ /* Length of the string EOL_STR points to. */
+ apr_size_t eol_str_len;
+
+ /* Buffer to cache any newline state between translation chunks */
+ char newline_buf[2];
+
+ /* Offset (within newline_buf) of the first *unused* character */
+ apr_size_t newline_off;
+
+ /* Buffer to cache keyword-parsing state between translation chunks */
+ char keyword_buf[SVN_KEYWORD_MAX_LEN];
+
+ /* Offset (within keyword-buf) to the first *unused* character */
+ apr_size_t keyword_off;
+
+ /* EOL style used in the chunk-source */
+ char src_format[2];
+
+ /* Length of the EOL style string found in the chunk-source,
+ or zero if none encountered yet */
+ apr_size_t src_format_len;
+
+ /* If this is svn_tristate_false, translate_newline() will be called
+ for every newline in the file */
+ svn_tristate_t nl_translation_skippable;
+};
+
+
+/* Allocate a baton for use with translate_chunk() in POOL and
+ * initialize it for the first iteration.
+ *
+ * The caller must assure that EOL_STR and KEYWORDS at least
+ * have the same life time as that of POOL.
+ */
+static struct translation_baton *
+create_translation_baton(const char *eol_str,
+ svn_boolean_t *translated_eol,
+ svn_boolean_t repair,
+ apr_hash_t *keywords,
+ svn_boolean_t expand,
+ apr_pool_t *pool)
+{
+ struct translation_baton *b = apr_palloc(pool, sizeof(*b));
+
+ /* For efficiency, convert an empty set of keywords to NULL. */
+ if (keywords && (apr_hash_count(keywords) == 0))
+ keywords = NULL;
+
+ b->eol_str = eol_str;
+ b->eol_str_len = eol_str ? strlen(eol_str) : 0;
+ b->translated_eol = translated_eol;
+ b->repair = repair;
+ b->keywords = keywords;
+ b->expand = expand;
+ b->newline_off = 0;
+ b->keyword_off = 0;
+ b->src_format_len = 0;
+ b->nl_translation_skippable = svn_tristate_unknown;
+
+ /* Most characters don't start translation actions.
+ * Mark those that do depending on the parameters we got. */
+ memset(b->interesting, FALSE, sizeof(b->interesting));
+ if (keywords)
+ b->interesting['$'] = TRUE;
+ if (eol_str)
+ {
+ b->interesting['\r'] = TRUE;
+ b->interesting['\n'] = TRUE;
+ }
+
+ return b;
+}
+
+/* Return TRUE if the EOL starting at BUF matches the eol_str member of B.
+ * Be aware of special cases like "\n\r\n" and "\n\n\r". For sequences like
+ * "\n$" (an EOL followed by a keyword), the result will be FALSE since it is
+ * more efficient to handle that special case implicitly in the calling code
+ * by exiting the quick scan loop.
+ * The caller must ensure that buf[0] and buf[1] refer to valid memory
+ * locations.
+ */
+static APR_INLINE svn_boolean_t
+eol_unchanged(struct translation_baton *b,
+ const char *buf)
+{
+ /* If the first byte doesn't match, the whole EOL won't.
+ * This does also handle the (certainly invalid) case that
+ * eol_str would be an empty string.
+ */
+ if (buf[0] != b->eol_str[0])
+ return FALSE;
+
+ /* two-char EOLs must be a full match */
+ if (b->eol_str_len == 2)
+ return buf[1] == b->eol_str[1];
+
+ /* The first char matches the required 1-byte EOL.
+ * But maybe, buf[] contains a 2-byte EOL?
+ * In that case, the second byte will be interesting
+ * and not be another EOL of its own.
+ */
+ return !b->interesting[(unsigned char)buf[1]] || buf[0] == buf[1];
+}
+
+
+/* Translate eols and keywords of a 'chunk' of characters BUF of size BUFLEN
+ * according to the settings and state stored in baton B.
+ *
+ * Write output to stream DST.
+ *
+ * To finish a series of chunk translations, flush all buffers by calling
+ * this routine with a NULL value for BUF.
+ *
+ * If B->translated_eol is not NULL, then set *B->translated_eol to TRUE if
+ * an end-of-line sequence was changed, otherwise leave it untouched.
+ *
+ * Use POOL for temporary allocations.
+ */
+static svn_error_t *
+translate_chunk(svn_stream_t *dst,
+ struct translation_baton *b,
+ const char *buf,
+ apr_size_t buflen,
+ apr_pool_t *pool)
+{
+ const char *p;
+ apr_size_t len;
+
+ if (buf)
+ {
+ /* precalculate some oft-used values */
+ const char *end = buf + buflen;
+ const char *interesting = b->interesting;
+ apr_size_t next_sign_off = 0;
+
+ /* At the beginning of this loop, assume that we might be in an
+ * interesting state, i.e. with data in the newline or keyword
+ * buffer. First try to get to the boring state so we can copy
+ * a run of boring characters; then try to get back to the
+ * interesting state by processing an interesting character,
+ * and repeat. */
+ for (p = buf; p < end;)
+ {
+ /* Try to get to the boring state, if necessary. */
+ if (b->newline_off)
+ {
+ if (*p == '\n')
+ b->newline_buf[b->newline_off++] = *p++;
+
+ SVN_ERR(translate_newline(b->eol_str, b->eol_str_len,
+ b->src_format,
+ &b->src_format_len, b->newline_buf,
+ b->newline_off, dst, b->translated_eol,
+ b->repair));
+
+ b->newline_off = 0;
+ }
+ else if (b->keyword_off && *p == '$')
+ {
+ svn_boolean_t keyword_matches;
+ char keyword_name[SVN_KEYWORD_MAX_LEN + 1];
+
+ /* If keyword is matched, but not correctly translated, try to
+ * look for the next ending '$'. */
+ b->keyword_buf[b->keyword_off++] = *p++;
+ keyword_matches = match_keyword(b->keyword_buf, b->keyword_off,
+ keyword_name, b->keywords);
+ if (!keyword_matches)
+ {
+ /* reuse the ending '$' */
+ p--;
+ b->keyword_off--;
+ }
+
+ if (!keyword_matches ||
+ translate_keyword(b->keyword_buf, &b->keyword_off,
+ keyword_name, b->expand, b->keywords) ||
+ b->keyword_off >= SVN_KEYWORD_MAX_LEN)
+ {
+ /* write out non-matching text or translated keyword */
+ SVN_ERR(translate_write(dst, b->keyword_buf, b->keyword_off));
+
+ next_sign_off = 0;
+ b->keyword_off = 0;
+ }
+ else
+ {
+ if (next_sign_off == 0)
+ next_sign_off = b->keyword_off - 1;
+
+ continue;
+ }
+ }
+ else if (b->keyword_off == SVN_KEYWORD_MAX_LEN - 1
+ || (b->keyword_off && (*p == '\r' || *p == '\n')))
+ {
+ if (next_sign_off > 0)
+ {
+ /* rolling back, continue with next '$' in keyword_buf */
+ p -= (b->keyword_off - next_sign_off);
+ b->keyword_off = next_sign_off;
+ next_sign_off = 0;
+ }
+ /* No closing '$' found; flush the keyword buffer. */
+ SVN_ERR(translate_write(dst, b->keyword_buf, b->keyword_off));
+
+ b->keyword_off = 0;
+ }
+ else if (b->keyword_off)
+ {
+ b->keyword_buf[b->keyword_off++] = *p++;
+ continue;
+ }
+
+ /* translate_newline will modify the baton for src_format_len==0
+ or may return an error if b->repair is FALSE. In all other
+ cases, we can skip the newline translation as long as source
+ EOL format and actual EOL format match. If there is a
+ mismatch, translate_newline will be called regardless of
+ nl_translation_skippable.
+ */
+ if (b->nl_translation_skippable == svn_tristate_unknown &&
+ b->src_format_len > 0)
+ {
+ /* test whether translate_newline may return an error */
+ if (b->eol_str_len == b->src_format_len &&
+ strncmp(b->eol_str, b->src_format, b->eol_str_len) == 0)
+ b->nl_translation_skippable = svn_tristate_true;
+ else if (b->repair)
+ b->nl_translation_skippable = svn_tristate_true;
+ else
+ b->nl_translation_skippable = svn_tristate_false;
+ }
+
+ /* We're in the boring state; look for interesting characters.
+ Offset len such that it will become 0 in the first iteration.
+ */
+ len = 0 - b->eol_str_len;
+
+ /* Look for the next EOL (or $) that actually needs translation.
+ Stop there or at EOF, whichever is encountered first.
+ */
+ do
+ {
+ /* skip current EOL */
+ len += b->eol_str_len;
+
+ /* Check 4 bytes at once to allow for efficient pipelining
+ and to reduce loop condition overhead. */
+ while ((p + len + 4) <= end)
+ {
+ if (interesting[(unsigned char)p[len]]
+ || interesting[(unsigned char)p[len+1]]
+ || interesting[(unsigned char)p[len+2]]
+ || interesting[(unsigned char)p[len+3]])
+ break;
+
+ len += 4;
+ }
+
+ /* Found an interesting char or EOF in the next 4 bytes.
+ Find its exact position. */
+ while ((p + len) < end && !interesting[(unsigned char)p[len]])
+ ++len;
+ }
+ while (b->nl_translation_skippable ==
+ svn_tristate_true && /* can potentially skip EOLs */
+ p + len + 2 < end && /* not too close to EOF */
+ eol_unchanged (b, p + len)); /* EOL format already ok */
+
+ while ((p + len) < end && !interesting[(unsigned char)p[len]])
+ len++;
+
+ if (len)
+ {
+ SVN_ERR(translate_write(dst, p, len));
+ p += len;
+ }
+
+ /* Set up state according to the interesting character, if any. */
+ if (p < end)
+ {
+ switch (*p)
+ {
+ case '$':
+ b->keyword_buf[b->keyword_off++] = *p++;
+ break;
+ case '\r':
+ b->newline_buf[b->newline_off++] = *p++;
+ break;
+ case '\n':
+ b->newline_buf[b->newline_off++] = *p++;
+
+ SVN_ERR(translate_newline(b->eol_str, b->eol_str_len,
+ b->src_format,
+ &b->src_format_len,
+ b->newline_buf,
+ b->newline_off, dst,
+ b->translated_eol, b->repair));
+
+ b->newline_off = 0;
+ break;
+
+ }
+ }
+ }
+ }
+ else
+ {
+ if (b->newline_off)
+ {
+ SVN_ERR(translate_newline(b->eol_str, b->eol_str_len,
+ b->src_format, &b->src_format_len,
+ b->newline_buf, b->newline_off,
+ dst, b->translated_eol, b->repair));
+ b->newline_off = 0;
+ }
+
+ if (b->keyword_off)
+ {
+ SVN_ERR(translate_write(dst, b->keyword_buf, b->keyword_off));
+ b->keyword_off = 0;
+ }
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Baton for use with translated stream callbacks. */
+struct translated_stream_baton
+{
+ /* Stream to take input from (before translation) on read
+ /write output to (after translation) on write. */
+ svn_stream_t *stream;
+
+ /* Input/Output translation batons to make them separate chunk streams. */
+ struct translation_baton *in_baton, *out_baton;
+
+ /* Remembers whether any write operations have taken place;
+ if so, we need to flush the output chunk stream. */
+ svn_boolean_t written;
+
+ /* Buffer to hold translated read data. */
+ svn_stringbuf_t *readbuf;
+
+ /* Offset of the first non-read character in readbuf. */
+ apr_size_t readbuf_off;
+
+ /* Buffer to hold read data
+ between svn_stream_read() and translate_chunk(). */
+ char *buf;
+#define SVN__TRANSLATION_BUF_SIZE (SVN__STREAM_CHUNK_SIZE + 1)
+
+ /* Pool for callback iterations */
+ apr_pool_t *iterpool;
+};
+
+
+/* Implements svn_read_fn_t. */
+static svn_error_t *
+translated_stream_read(void *baton,
+ char *buffer,
+ apr_size_t *len)
+{
+ struct translated_stream_baton *b = baton;
+ apr_size_t readlen = SVN__STREAM_CHUNK_SIZE;
+ apr_size_t unsatisfied = *len;
+ apr_size_t off = 0;
+
+ /* Optimization for a frequent special case. The configuration parser (and
+ a few others) reads the stream one byte at a time. All the memcpy, pool
+ clearing etc. imposes a huge overhead in that case. In most cases, we
+ can just take that single byte directly from the read buffer.
+
+ Since *len > 1 requires lots of code to be run anyways, we can afford
+ the extra overhead of checking for *len == 1.
+
+ See <http://mail-archives.apache.org/mod_mbox/subversion-dev/201003.mbox/%3C4B94011E.1070207@alice-dsl.de%3E>.
+ */
+ if (unsatisfied == 1 && b->readbuf_off < b->readbuf->len)
+ {
+ /* Just take it from the read buffer */
+ *buffer = b->readbuf->data[b->readbuf_off++];
+
+ return SVN_NO_ERROR;
+ }
+
+ /* Standard code path. */
+ while (readlen == SVN__STREAM_CHUNK_SIZE && unsatisfied > 0)
+ {
+ apr_size_t to_copy;
+ apr_size_t buffer_remainder;
+
+ svn_pool_clear(b->iterpool);
+ /* fill read buffer, if necessary */
+ if (! (b->readbuf_off < b->readbuf->len))
+ {
+ svn_stream_t *buf_stream;
+
+ svn_stringbuf_setempty(b->readbuf);
+ b->readbuf_off = 0;
+ SVN_ERR(svn_stream_read(b->stream, b->buf, &readlen));
+ buf_stream = svn_stream_from_stringbuf(b->readbuf, b->iterpool);
+
+ SVN_ERR(translate_chunk(buf_stream, b->in_baton, b->buf,
+ readlen, b->iterpool));
+
+ if (readlen != SVN__STREAM_CHUNK_SIZE)
+ SVN_ERR(translate_chunk(buf_stream, b->in_baton, NULL, 0,
+ b->iterpool));
+
+ SVN_ERR(svn_stream_close(buf_stream));
+ }
+
+ /* Satisfy from the read buffer */
+ buffer_remainder = b->readbuf->len - b->readbuf_off;
+ to_copy = (buffer_remainder > unsatisfied)
+ ? unsatisfied : buffer_remainder;
+ memcpy(buffer + off, b->readbuf->data + b->readbuf_off, to_copy);
+ off += to_copy;
+ b->readbuf_off += to_copy;
+ unsatisfied -= to_copy;
+ }
+
+ *len -= unsatisfied;
+
+ return SVN_NO_ERROR;
+}
+
+/* Implements svn_write_fn_t. */
+static svn_error_t *
+translated_stream_write(void *baton,
+ const char *buffer,
+ apr_size_t *len)
+{
+ struct translated_stream_baton *b = baton;
+ svn_pool_clear(b->iterpool);
+
+ b->written = TRUE;
+ return translate_chunk(b->stream, b->out_baton, buffer, *len, b->iterpool);
+}
+
+/* Implements svn_close_fn_t. */
+static svn_error_t *
+translated_stream_close(void *baton)
+{
+ struct translated_stream_baton *b = baton;
+ svn_error_t *err = NULL;
+
+ if (b->written)
+ err = translate_chunk(b->stream, b->out_baton, NULL, 0, b->iterpool);
+
+ err = svn_error_compose_create(err, svn_stream_close(b->stream));
+
+ svn_pool_destroy(b->iterpool);
+
+ return svn_error_trace(err);
+}
+
+
+/* svn_stream_mark_t for translation streams. */
+typedef struct mark_translated_t
+{
+ /* Saved translation state. */
+ struct translated_stream_baton saved_baton;
+
+ /* Mark set on the underlying stream. */
+ svn_stream_mark_t *mark;
+} mark_translated_t;
+
+/* Implements svn_stream_mark_fn_t. */
+static svn_error_t *
+translated_stream_mark(void *baton, svn_stream_mark_t **mark, apr_pool_t *pool)
+{
+ mark_translated_t *mt;
+ struct translated_stream_baton *b = baton;
+
+ mt = apr_palloc(pool, sizeof(*mt));
+ SVN_ERR(svn_stream_mark(b->stream, &mt->mark, pool));
+
+ /* Save translation state. */
+ mt->saved_baton.in_baton = apr_pmemdup(pool, b->in_baton,
+ sizeof(*mt->saved_baton.in_baton));
+ mt->saved_baton.out_baton = apr_pmemdup(pool, b->out_baton,
+ sizeof(*mt->saved_baton.out_baton));
+ mt->saved_baton.written = b->written;
+ mt->saved_baton.readbuf = svn_stringbuf_dup(b->readbuf, pool);
+ mt->saved_baton.readbuf_off = b->readbuf_off;
+ mt->saved_baton.buf = apr_pmemdup(pool, b->buf, SVN__TRANSLATION_BUF_SIZE);
+
+ *mark = (svn_stream_mark_t *)mt;
+
+ return SVN_NO_ERROR;
+}
+
+/* Implements svn_stream_seek_fn_t. */
+static svn_error_t *
+translated_stream_seek(void *baton, const svn_stream_mark_t *mark)
+{
+ struct translated_stream_baton *b = baton;
+
+ if (mark != NULL)
+ {
+ const mark_translated_t *mt = (const mark_translated_t *)mark;
+
+ /* Flush output buffer if necessary. */
+ if (b->written)
+ SVN_ERR(translate_chunk(b->stream, b->out_baton, NULL, 0,
+ b->iterpool));
+
+ SVN_ERR(svn_stream_seek(b->stream, mt->mark));
+
+ /* Restore translation state, avoiding new allocations. */
+ *b->in_baton = *mt->saved_baton.in_baton;
+ *b->out_baton = *mt->saved_baton.out_baton;
+ b->written = mt->saved_baton.written;
+ svn_stringbuf_setempty(b->readbuf);
+ svn_stringbuf_appendbytes(b->readbuf, mt->saved_baton.readbuf->data,
+ mt->saved_baton.readbuf->len);
+ b->readbuf_off = mt->saved_baton.readbuf_off;
+ memcpy(b->buf, mt->saved_baton.buf, SVN__TRANSLATION_BUF_SIZE);
+ }
+ else
+ {
+ SVN_ERR(svn_stream_reset(b->stream));
+
+ b->in_baton->newline_off = 0;
+ b->in_baton->keyword_off = 0;
+ b->in_baton->src_format_len = 0;
+ b->out_baton->newline_off = 0;
+ b->out_baton->keyword_off = 0;
+ b->out_baton->src_format_len = 0;
+
+ b->written = FALSE;
+ svn_stringbuf_setempty(b->readbuf);
+ b->readbuf_off = 0;
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Implements svn_stream__is_buffered_fn_t. */
+static svn_boolean_t
+translated_stream_is_buffered(void *baton)
+{
+ struct translated_stream_baton *b = baton;
+ return svn_stream__is_buffered(b->stream);
+}
+
+svn_error_t *
+svn_subst_read_specialfile(svn_stream_t **stream,
+ const char *path,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ apr_finfo_t finfo;
+ svn_string_t *buf;
+
+ /* First determine what type of special file we are
+ detranslating. */
+ SVN_ERR(svn_io_stat(&finfo, path, APR_FINFO_MIN | APR_FINFO_LINK,
+ scratch_pool));
+
+ switch (finfo.filetype) {
+ case APR_REG:
+ /* Nothing special to do here, just create stream from the original
+ file's contents. */
+ SVN_ERR(svn_stream_open_readonly(stream, path, result_pool, scratch_pool));
+ break;
+
+ case APR_LNK:
+ /* Determine the destination of the link. */
+ SVN_ERR(svn_io_read_link(&buf, path, scratch_pool));
+ *stream = svn_stream_from_string(svn_string_createf(result_pool,
+ "link %s",
+ buf->data),
+ result_pool);
+ break;
+
+ default:
+ SVN_ERR_MALFUNCTION();
+ }
+
+ return SVN_NO_ERROR;
+}
+
+/* Same as svn_subst_stream_translated(), except for the following.
+ *
+ * If TRANSLATED_EOL is not NULL, then reading and/or writing to the stream
+ * will set *TRANSLATED_EOL to TRUE if an end-of-line sequence was changed,
+ * otherwise leave it untouched.
+ */
+static svn_stream_t *
+stream_translated(svn_stream_t *stream,
+ const char *eol_str,
+ svn_boolean_t *translated_eol,
+ svn_boolean_t repair,
+ apr_hash_t *keywords,
+ svn_boolean_t expand,
+ apr_pool_t *result_pool)
+{
+ struct translated_stream_baton *baton
+ = apr_palloc(result_pool, sizeof(*baton));
+ svn_stream_t *s = svn_stream_create(baton, result_pool);
+
+ /* Make sure EOL_STR and KEYWORDS are allocated in RESULT_POOL
+ so they have the same lifetime as the stream. */
+ if (eol_str)
+ eol_str = apr_pstrdup(result_pool, eol_str);
+ if (keywords)
+ {
+ if (apr_hash_count(keywords) == 0)
+ keywords = NULL;
+ else
+ {
+ /* deep copy the hash to make sure it's allocated in RESULT_POOL */
+ apr_hash_t *copy = apr_hash_make(result_pool);
+ apr_hash_index_t *hi;
+ apr_pool_t *subpool;
+
+ subpool = svn_pool_create(result_pool);
+ for (hi = apr_hash_first(subpool, keywords);
+ hi; hi = apr_hash_next(hi))
+ {
+ const void *key;
+ void *val;
+
+ apr_hash_this(hi, &key, NULL, &val);
+ svn_hash_sets(copy, apr_pstrdup(result_pool, key),
+ svn_string_dup(val, result_pool));
+ }
+ svn_pool_destroy(subpool);
+
+ keywords = copy;
+ }
+ }
+
+ /* Setup the baton fields */
+ baton->stream = stream;
+ baton->in_baton
+ = create_translation_baton(eol_str, translated_eol, repair, keywords,
+ expand, result_pool);
+ baton->out_baton
+ = create_translation_baton(eol_str, translated_eol, repair, keywords,
+ expand, result_pool);
+ baton->written = FALSE;
+ baton->readbuf = svn_stringbuf_create_empty(result_pool);
+ baton->readbuf_off = 0;
+ baton->iterpool = svn_pool_create(result_pool);
+ baton->buf = apr_palloc(result_pool, SVN__TRANSLATION_BUF_SIZE);
+
+ /* Setup the stream methods */
+ svn_stream_set_read(s, translated_stream_read);
+ svn_stream_set_write(s, translated_stream_write);
+ svn_stream_set_close(s, translated_stream_close);
+ svn_stream_set_mark(s, translated_stream_mark);
+ svn_stream_set_seek(s, translated_stream_seek);
+ svn_stream__set_is_buffered(s, translated_stream_is_buffered);
+
+ return s;
+}
+
+svn_stream_t *
+svn_subst_stream_translated(svn_stream_t *stream,
+ const char *eol_str,
+ svn_boolean_t repair,
+ apr_hash_t *keywords,
+ svn_boolean_t expand,
+ apr_pool_t *result_pool)
+{
+ return stream_translated(stream, eol_str, NULL, repair, keywords, expand,
+ result_pool);
+}
+
+/* Same as svn_subst_translate_cstring2(), except for the following.
+ *
+ * If TRANSLATED_EOL is not NULL, then set *TRANSLATED_EOL to TRUE if an
+ * end-of-line sequence was changed, or to FALSE otherwise.
+ */
+static svn_error_t *
+translate_cstring(const char **dst,
+ svn_boolean_t *translated_eol,
+ const char *src,
+ const char *eol_str,
+ svn_boolean_t repair,
+ apr_hash_t *keywords,
+ svn_boolean_t expand,
+ apr_pool_t *pool)
+{
+ svn_stringbuf_t *dst_stringbuf;
+ svn_stream_t *dst_stream;
+ apr_size_t len = strlen(src);
+
+ /* The easy way out: no translation needed, just copy. */
+ if (! (eol_str || (keywords && (apr_hash_count(keywords) > 0))))
+ {
+ *dst = apr_pstrmemdup(pool, src, len);
+ return SVN_NO_ERROR;
+ }
+
+ /* Create a stringbuf and wrapper stream to hold the output. */
+ dst_stringbuf = svn_stringbuf_create_empty(pool);
+ dst_stream = svn_stream_from_stringbuf(dst_stringbuf, pool);
+
+ if (translated_eol)
+ *translated_eol = FALSE;
+
+ /* Another wrapper to translate the content. */
+ dst_stream = stream_translated(dst_stream, eol_str, translated_eol, repair,
+ keywords, expand, pool);
+
+ /* Jam the text into the destination stream (to translate it). */
+ SVN_ERR(svn_stream_write(dst_stream, src, &len));
+
+ /* Close the destination stream to flush unwritten data. */
+ SVN_ERR(svn_stream_close(dst_stream));
+
+ *dst = dst_stringbuf->data;
+ return SVN_NO_ERROR;
+}
+
+svn_error_t *
+svn_subst_translate_cstring2(const char *src,
+ const char **dst,
+ const char *eol_str,
+ svn_boolean_t repair,
+ apr_hash_t *keywords,
+ svn_boolean_t expand,
+ apr_pool_t *pool)
+{
+ return translate_cstring(dst, NULL, src, eol_str, repair, keywords, expand,
+ pool);
+}
+
+/* Given a special file at SRC, generate a textual representation of
+ it in a normal file at DST. Perform all allocations in POOL. */
+/* ### this should be folded into svn_subst_copy_and_translate3 */
+static svn_error_t *
+detranslate_special_file(const char *src, const char *dst,
+ svn_cancel_func_t cancel_func, void *cancel_baton,
+ apr_pool_t *scratch_pool)
+{
+ const char *dst_tmp;
+ svn_stream_t *src_stream;
+ svn_stream_t *dst_stream;
+
+ /* Open a temporary destination that we will eventually atomically
+ rename into place. */
+ SVN_ERR(svn_stream_open_unique(&dst_stream, &dst_tmp,
+ svn_dirent_dirname(dst, scratch_pool),
+ svn_io_file_del_none,
+ scratch_pool, scratch_pool));
+ SVN_ERR(svn_subst_read_specialfile(&src_stream, src,
+ scratch_pool, scratch_pool));
+ SVN_ERR(svn_stream_copy3(src_stream, dst_stream,
+ cancel_func, cancel_baton, scratch_pool));
+
+ /* Do the atomic rename from our temporary location. */
+ return svn_error_trace(svn_io_file_rename(dst_tmp, dst, scratch_pool));
+}
+
+/* Creates a special file DST from the "normal form" located in SOURCE.
+ *
+ * All temporary allocations will be done in POOL.
+ */
+static svn_error_t *
+create_special_file_from_stream(svn_stream_t *source, const char *dst,
+ apr_pool_t *pool)
+{
+ svn_stringbuf_t *contents;
+ svn_boolean_t eof;
+ const char *identifier;
+ const char *remainder;
+ const char *dst_tmp;
+ svn_boolean_t create_using_internal_representation = FALSE;
+
+ SVN_ERR(svn_stream_readline(source, &contents, "\n", &eof, pool));
+
+ /* Separate off the identifier. The first space character delimits
+ the identifier, after which any remaining characters are specific
+ to the actual special file type being created. */
+ identifier = contents->data;
+ for (remainder = identifier; *remainder; remainder++)
+ {
+ if (*remainder == ' ')
+ {
+ remainder++;
+ break;
+ }
+ }
+
+ if (! strncmp(identifier, SVN_SUBST__SPECIAL_LINK_STR " ",
+ sizeof(SVN_SUBST__SPECIAL_LINK_STR " ")-1))
+ {
+ /* For symlinks, the type specific data is just a filesystem
+ path that the symlink should reference. */
+ svn_error_t *err = svn_io_create_unique_link(&dst_tmp, dst, remainder,
+ ".tmp", pool);
+
+ /* If we had an error, check to see if it was because symlinks are
+ not supported on the platform. If so, fall back
+ to using the internal representation. */
+ if (err)
+ {
+ if (err->apr_err == SVN_ERR_UNSUPPORTED_FEATURE)
+ {
+ svn_error_clear(err);
+ create_using_internal_representation = TRUE;
+ }
+ else
+ return err;
+ }
+ }
+ else
+ {
+ /* Just create a normal file using the internal special file
+ representation. We don't want a commit of an unknown special
+ file type to DoS all the clients. */
+ create_using_internal_representation = TRUE;
+ }
+
+ /* If nothing else worked, write out the internal representation to
+ a file that can be edited by the user.
+
+ ### this only writes the first line!
+ */
+ if (create_using_internal_representation)
+ SVN_ERR(svn_io_write_unique(&dst_tmp, svn_dirent_dirname(dst, pool),
+ contents->data, contents->len,
+ svn_io_file_del_none, pool));
+
+ /* Do the atomic rename from our temporary location. */
+ return svn_io_file_rename(dst_tmp, dst, pool);
+}
+
+
+svn_error_t *
+svn_subst_copy_and_translate4(const char *src,
+ const char *dst,
+ const char *eol_str,
+ svn_boolean_t repair,
+ apr_hash_t *keywords,
+ svn_boolean_t expand,
+ svn_boolean_t special,
+ svn_cancel_func_t cancel_func,
+ void *cancel_baton,
+ apr_pool_t *pool)
+{
+ svn_stream_t *src_stream;
+ svn_stream_t *dst_stream;
+ const char *dst_tmp;
+ svn_error_t *err;
+ svn_node_kind_t kind;
+ svn_boolean_t path_special;
+
+ SVN_ERR(svn_io_check_special_path(src, &kind, &path_special, pool));
+
+ /* If this is a 'special' file, we may need to create it or
+ detranslate it. */
+ if (special || path_special)
+ {
+ if (expand)
+ {
+ if (path_special)
+ {
+ /* We are being asked to create a special file from a special
+ file. Do a temporary detranslation and work from there. */
+
+ /* ### woah. this section just undoes all the work we already did
+ ### to read the contents of the special file. shoot... the
+ ### svn_subst_read_specialfile even checks the file type
+ ### for us! */
+
+ SVN_ERR(svn_subst_read_specialfile(&src_stream, src, pool, pool));
+ }
+ else
+ {
+ SVN_ERR(svn_stream_open_readonly(&src_stream, src, pool, pool));
+ }
+
+ return svn_error_trace(create_special_file_from_stream(src_stream,
+ dst, pool));
+ }
+ /* else !expand */
+
+ return svn_error_trace(detranslate_special_file(src, dst,
+ cancel_func,
+ cancel_baton,
+ pool));
+ }
+
+ /* The easy way out: no translation needed, just copy. */
+ if (! (eol_str || (keywords && (apr_hash_count(keywords) > 0))))
+ return svn_error_trace(svn_io_copy_file(src, dst, FALSE, pool));
+
+ /* Open source file. */
+ SVN_ERR(svn_stream_open_readonly(&src_stream, src, pool, pool));
+
+ /* For atomicity, we translate to a tmp file and then rename the tmp file
+ over the real destination. */
+ SVN_ERR(svn_stream_open_unique(&dst_stream, &dst_tmp,
+ svn_dirent_dirname(dst, pool),
+ svn_io_file_del_none, pool, pool));
+
+ dst_stream = svn_subst_stream_translated(dst_stream, eol_str, repair,
+ keywords, expand, pool);
+
+ /* ###: use cancel func/baton in place of NULL/NULL below. */
+ err = svn_stream_copy3(src_stream, dst_stream, cancel_func, cancel_baton,
+ pool);
+ if (err)
+ {
+ /* On errors, we have a pathname available. */
+ if (err->apr_err == SVN_ERR_IO_INCONSISTENT_EOL)
+ err = svn_error_createf(SVN_ERR_IO_INCONSISTENT_EOL, err,
+ _("File '%s' has inconsistent newlines"),
+ svn_dirent_local_style(src, pool));
+ return svn_error_compose_create(err, svn_io_remove_file2(dst_tmp,
+ FALSE, pool));
+ }
+
+ /* Now that dst_tmp contains the translated data, do the atomic rename. */
+ SVN_ERR(svn_io_file_rename(dst_tmp, dst, pool));
+
+ /* Preserve the source file's permission bits. */
+ SVN_ERR(svn_io_copy_perms(src, dst, pool));
+
+ return SVN_NO_ERROR;
+}
+
+
+/*** 'Special file' stream support */
+
+struct special_stream_baton
+{
+ svn_stream_t *read_stream;
+ svn_stringbuf_t *write_content;
+ svn_stream_t *write_stream;
+ const char *path;
+ apr_pool_t *pool;
+};
+
+
+static svn_error_t *
+read_handler_special(void *baton, char *buffer, apr_size_t *len)
+{
+ struct special_stream_baton *btn = baton;
+
+ if (btn->read_stream)
+ /* We actually found a file to read from */
+ return svn_stream_read(btn->read_stream, buffer, len);
+ else
+ return svn_error_createf(APR_ENOENT, NULL,
+ "Can't read special file: File '%s' not found",
+ svn_dirent_local_style(btn->path, btn->pool));
+}
+
+static svn_error_t *
+write_handler_special(void *baton, const char *buffer, apr_size_t *len)
+{
+ struct special_stream_baton *btn = baton;
+
+ return svn_stream_write(btn->write_stream, buffer, len);
+}
+
+
+static svn_error_t *
+close_handler_special(void *baton)
+{
+ struct special_stream_baton *btn = baton;
+
+ if (btn->write_content->len)
+ {
+ /* yeay! we received data and need to create a special file! */
+
+ svn_stream_t *source = svn_stream_from_stringbuf(btn->write_content,
+ btn->pool);
+ SVN_ERR(create_special_file_from_stream(source, btn->path, btn->pool));
+ }
+
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_subst_create_specialfile(svn_stream_t **stream,
+ const char *path,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ struct special_stream_baton *baton = apr_palloc(result_pool, sizeof(*baton));
+
+ baton->path = apr_pstrdup(result_pool, path);
+
+ /* SCRATCH_POOL may not exist after the function returns. */
+ baton->pool = result_pool;
+
+ baton->write_content = svn_stringbuf_create_empty(result_pool);
+ baton->write_stream = svn_stream_from_stringbuf(baton->write_content,
+ result_pool);
+
+ *stream = svn_stream_create(baton, result_pool);
+ svn_stream_set_write(*stream, write_handler_special);
+ svn_stream_set_close(*stream, close_handler_special);
+
+ return SVN_NO_ERROR;
+}
+
+
+/* NOTE: this function is deprecated, but we cannot move it over to
+ deprecated.c because it uses stuff private to this file, and it is
+ not easily rebuilt in terms of "new" functions. */
+svn_error_t *
+svn_subst_stream_from_specialfile(svn_stream_t **stream,
+ const char *path,
+ apr_pool_t *pool)
+{
+ struct special_stream_baton *baton = apr_palloc(pool, sizeof(*baton));
+ svn_error_t *err;
+
+ baton->pool = pool;
+ baton->path = apr_pstrdup(pool, path);
+
+ err = svn_subst_read_specialfile(&baton->read_stream, path, pool, pool);
+
+ /* File might not exist because we intend to create it upon close. */
+ if (err && APR_STATUS_IS_ENOENT(err->apr_err))
+ {
+ svn_error_clear(err);
+
+ /* Note: the special file is missing. the caller won't find out
+ until the first read. Oh well. This function is deprecated anyways,
+ so they can just deal with the weird behavior. */
+ baton->read_stream = NULL;
+ }
+
+ baton->write_content = svn_stringbuf_create_empty(pool);
+ baton->write_stream = svn_stream_from_stringbuf(baton->write_content, pool);
+
+ *stream = svn_stream_create(baton, pool);
+ svn_stream_set_read(*stream, read_handler_special);
+ svn_stream_set_write(*stream, write_handler_special);
+ svn_stream_set_close(*stream, close_handler_special);
+
+ return SVN_NO_ERROR;
+}
+
+
+
+/*** String translation */
+svn_error_t *
+svn_subst_translate_string2(svn_string_t **new_value,
+ svn_boolean_t *translated_to_utf8,
+ svn_boolean_t *translated_line_endings,
+ const svn_string_t *value,
+ const char *encoding,
+ svn_boolean_t repair,
+ apr_pool_t *result_pool,
+ apr_pool_t *scratch_pool)
+{
+ const char *val_utf8;
+ const char *val_utf8_lf;
+
+ if (value == NULL)
+ {
+ *new_value = NULL;
+ return SVN_NO_ERROR;
+ }
+
+ if (encoding)
+ {
+ SVN_ERR(svn_utf_cstring_to_utf8_ex2(&val_utf8, value->data,
+ encoding, scratch_pool));
+ }
+ else
+ {
+ SVN_ERR(svn_utf_cstring_to_utf8(&val_utf8, value->data, scratch_pool));
+ }
+
+ if (translated_to_utf8)
+ *translated_to_utf8 = (strcmp(value->data, val_utf8) != 0);
+
+ SVN_ERR(translate_cstring(&val_utf8_lf,
+ translated_line_endings,
+ val_utf8,
+ "\n", /* translate to LF */
+ repair,
+ NULL, /* no keywords */
+ FALSE, /* no expansion */
+ scratch_pool));
+
+ *new_value = svn_string_create(val_utf8_lf, result_pool);
+ return SVN_NO_ERROR;
+}
+
+
+svn_error_t *
+svn_subst_detranslate_string(svn_string_t **new_value,
+ const svn_string_t *value,
+ svn_boolean_t for_output,
+ apr_pool_t *pool)
+{
+ svn_error_t *err;
+ const char *val_neol;
+ const char *val_nlocale_neol;
+
+ if (value == NULL)
+ {
+ *new_value = NULL;
+ return SVN_NO_ERROR;
+ }
+
+ SVN_ERR(svn_subst_translate_cstring2(value->data,
+ &val_neol,
+ APR_EOL_STR, /* 'native' eol */
+ FALSE, /* no repair */
+ NULL, /* no keywords */
+ FALSE, /* no expansion */
+ pool));
+
+ if (for_output)
+ {
+ err = svn_cmdline_cstring_from_utf8(&val_nlocale_neol, val_neol, pool);
+ if (err && (APR_STATUS_IS_EINVAL(err->apr_err)))
+ {
+ val_nlocale_neol =
+ svn_cmdline_cstring_from_utf8_fuzzy(val_neol, pool);
+ svn_error_clear(err);
+ }
+ else if (err)
+ return err;
+ }
+ else
+ {
+ err = svn_utf_cstring_from_utf8(&val_nlocale_neol, val_neol, pool);
+ if (err && (APR_STATUS_IS_EINVAL(err->apr_err)))
+ {
+ val_nlocale_neol = svn_utf_cstring_from_utf8_fuzzy(val_neol, pool);
+ svn_error_clear(err);
+ }
+ else if (err)
+ return err;
+ }
+
+ *new_value = svn_string_create(val_nlocale_neol, pool);
+
+ return SVN_NO_ERROR;
+}
OpenPOWER on IntegriCloud