diff options
Diffstat (limited to 'subversion/libsvn_subr/subst.c')
-rw-r--r-- | subversion/libsvn_subr/subst.c | 2025 |
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; +} |