blob: a1b5b8b6e2362ca41220309183285496252a0125 [file] [log] [blame]
/*
* replay.c: mod_dav_svn REPORT handler for replaying revisions
*
* ====================================================================
* 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.
* ====================================================================
*/
#include <apr_pools.h>
#include <apr_strings.h>
#include <apr_xml.h>
#include <http_request.h>
#include <http_log.h>
#include <mod_dav.h>
#include "svn_pools.h"
#include "svn_repos.h"
#include "svn_fs.h"
#include "svn_base64.h"
#include "svn_xml.h"
#include "svn_path.h"
#include "svn_dav.h"
#include "svn_props.h"
#include "private/svn_log.h"
#include "../dav_svn.h"
typedef struct edit_baton_t {
apr_bucket_brigade *bb;
dav_svn__output *output;
svn_boolean_t started;
svn_boolean_t sending_textdelta;
int compression_level;
int svndiff_version;
} edit_baton_t;
/*** Helper Functions ***/
static svn_error_t *
maybe_start_report(edit_baton_t *eb)
{
if (! eb->started)
{
SVN_ERR(dav_svn__brigade_puts(eb->bb, eb->output,
DAV_XML_HEADER DEBUG_CR
"<S:editor-report xmlns:S=\""
SVN_XML_NAMESPACE "\">" DEBUG_CR));
eb->started = TRUE;
}
return SVN_NO_ERROR;
}
static svn_error_t *
end_report(edit_baton_t *eb)
{
SVN_ERR(dav_svn__brigade_puts(eb->bb, eb->output,
"</S:editor-report>" DEBUG_CR));
return SVN_NO_ERROR;
}
static svn_error_t *
maybe_close_textdelta(edit_baton_t *eb)
{
if (eb->sending_textdelta)
{
SVN_ERR(dav_svn__brigade_puts(eb->bb, eb->output,
"</S:apply-textdelta>" DEBUG_CR));
eb->sending_textdelta = FALSE;
}
return SVN_NO_ERROR;
}
static svn_error_t *
add_file_or_directory(const char *file_or_directory,
const char *path,
edit_baton_t *eb,
const char *copyfrom_path,
svn_revnum_t copyfrom_rev,
apr_pool_t *pool,
void **added_baton)
{
const char *qname = apr_xml_quote_string(pool, path, 1);
const char *qcopy =
copyfrom_path ? apr_xml_quote_string(pool, copyfrom_path, 1) : NULL;
SVN_ERR(maybe_close_textdelta(eb));
*added_baton = eb;
if (! copyfrom_path)
SVN_ERR(dav_svn__brigade_printf(eb->bb, eb->output,
"<S:add-%s name=\"%s\"/>" DEBUG_CR,
file_or_directory, qname));
else
SVN_ERR(dav_svn__brigade_printf(eb->bb, eb->output,
"<S:add-%s name=\"%s\" "
"copyfrom-path=\"%s\" "
"copyfrom-rev=\"%ld\"/>" DEBUG_CR,
file_or_directory, qname,
qcopy, copyfrom_rev));
return SVN_NO_ERROR;
}
static svn_error_t *
open_file_or_directory(const char *file_or_directory,
const char *path,
edit_baton_t *eb,
svn_revnum_t base_revision,
apr_pool_t *pool,
void **opened_baton)
{
const char *qname = apr_xml_quote_string(pool, path, 1);
SVN_ERR(maybe_close_textdelta(eb));
*opened_baton = eb;
return dav_svn__brigade_printf(eb->bb, eb->output,
"<S:open-%s name=\"%s\" rev=\"%ld\"/>"
DEBUG_CR,
file_or_directory, qname, base_revision);
}
static svn_error_t *
change_file_or_dir_prop(const char *file_or_dir,
edit_baton_t *eb,
const char *name,
const svn_string_t *value,
apr_pool_t *pool)
{
const char *qname = apr_xml_quote_string(pool, name, 1);
SVN_ERR(maybe_close_textdelta(eb));
if (value)
{
const svn_string_t *enc_value =
svn_base64_encode_string2(value, TRUE, pool);
/* Some versions of apr_brigade_vprintf() have a buffer overflow
bug that can be triggered by just the wrong size of a large
property value. The bug has been fixed (see
http://svn.apache.org/viewvc?view=rev&revision=768417), but
we need a workaround for the buggy APR versions, so we write
our potentially large block of property data using a
different underlying function. */
SVN_ERR(dav_svn__brigade_printf(eb->bb, eb->output,
"<S:change-%s-prop name=\"%s\">",
file_or_dir, qname));
SVN_ERR(dav_svn__brigade_write(eb->bb, eb->output,
enc_value->data, enc_value->len));
SVN_ERR(dav_svn__brigade_printf(eb->bb, eb->output,
"</S:change-%s-prop>" DEBUG_CR,
file_or_dir));
}
else
{
SVN_ERR(dav_svn__brigade_printf
(eb->bb, eb->output,
"<S:change-%s-prop name=\"%s\" del=\"true\"/>" DEBUG_CR,
file_or_dir, qname));
}
return SVN_NO_ERROR;
}
/*** Editor Implementation ***/
static svn_error_t *
set_target_revision(void *edit_baton,
svn_revnum_t target_revision,
apr_pool_t *pool)
{
edit_baton_t *eb = edit_baton;
SVN_ERR(maybe_start_report(eb));
return dav_svn__brigade_printf(eb->bb, eb->output,
"<S:target-revision rev=\"%ld\"/>" DEBUG_CR,
target_revision);
}
static svn_error_t *
open_root(void *edit_baton,
svn_revnum_t base_revision,
apr_pool_t *pool,
void **root_baton)
{
edit_baton_t *eb = edit_baton;
*root_baton = edit_baton;
SVN_ERR(maybe_start_report(eb));
return dav_svn__brigade_printf(eb->bb, eb->output,
"<S:open-root rev=\"%ld\"/>" DEBUG_CR,
base_revision);
}
static svn_error_t *
delete_entry(const char *path,
svn_revnum_t revision,
void *parent_baton,
apr_pool_t *pool)
{
edit_baton_t *eb = parent_baton;
const char *qname = apr_xml_quote_string(pool, path, 1);
SVN_ERR(maybe_close_textdelta(eb));
return dav_svn__brigade_printf(eb->bb, eb->output,
"<S:delete-entry name=\"%s\" rev=\"%ld\"/>"
DEBUG_CR,
qname, revision);
}
static svn_error_t *
add_directory(const char *path,
void *parent_baton,
const char *copyfrom_path,
svn_revnum_t copyfrom_rev,
apr_pool_t *pool,
void **child_baton)
{
return add_file_or_directory("directory", path, parent_baton,
copyfrom_path, copyfrom_rev, pool, child_baton);
}
static svn_error_t *
open_directory(const char *path,
void *parent_baton,
svn_revnum_t base_revision,
apr_pool_t *pool,
void **child_baton)
{
return open_file_or_directory("directory", path, parent_baton,
base_revision, pool, child_baton);
}
static svn_error_t *
change_dir_prop(void *baton,
const char *name,
const svn_string_t *value,
apr_pool_t *pool)
{
return change_file_or_dir_prop("dir", baton, name, value, pool);
}
static svn_error_t *
add_file(const char *path,
void *parent_baton,
const char *copyfrom_path,
svn_revnum_t copyfrom_rev,
apr_pool_t *pool,
void **file_baton)
{
return add_file_or_directory("file", path, parent_baton,
copyfrom_path, copyfrom_rev, pool, file_baton);
}
static svn_error_t *
open_file(const char *path,
void *parent_baton,
svn_revnum_t base_revision,
apr_pool_t *pool,
void **file_baton)
{
return open_file_or_directory("file", path, parent_baton,
base_revision, pool, file_baton);
}
static svn_error_t *
change_file_prop(void *baton,
const char *name,
const svn_string_t *value,
apr_pool_t *pool)
{
return change_file_or_dir_prop("file", baton, name, value, pool);
}
static svn_error_t *
apply_textdelta(void *file_baton,
const char *base_checksum,
apr_pool_t *pool,
svn_txdelta_window_handler_t *handler,
void **handler_baton)
{
edit_baton_t *eb = file_baton;
SVN_ERR(dav_svn__brigade_puts(eb->bb, eb->output, "<S:apply-textdelta"));
if (base_checksum)
SVN_ERR(dav_svn__brigade_printf(eb->bb, eb->output, " checksum=\"%s\">",
base_checksum));
else
SVN_ERR(dav_svn__brigade_puts(eb->bb, eb->output, ">"));
svn_txdelta_to_svndiff3(handler,
handler_baton,
dav_svn__make_base64_output_stream(eb->bb,
eb->output,
pool),
eb->svndiff_version,
eb->compression_level,
pool);
eb->sending_textdelta = TRUE;
return SVN_NO_ERROR;
}
static svn_error_t *
close_file(void *file_baton, const char *text_checksum, apr_pool_t *pool)
{
edit_baton_t *eb = file_baton;
SVN_ERR(maybe_close_textdelta(eb));
SVN_ERR(dav_svn__brigade_puts(eb->bb, eb->output, "<S:close-file"));
if (text_checksum)
SVN_ERR(dav_svn__brigade_printf(eb->bb, eb->output,
" checksum=\"%s\"/>" DEBUG_CR,
text_checksum));
else
SVN_ERR(dav_svn__brigade_puts(eb->bb, eb->output, "/>" DEBUG_CR));
return SVN_NO_ERROR;
}
static svn_error_t *
close_directory(void *dir_baton, apr_pool_t *pool)
{
edit_baton_t *eb = dir_baton;
return dav_svn__brigade_puts(eb->bb, eb->output,
"<S:close-directory/>" DEBUG_CR);
}
static void
make_editor(const svn_delta_editor_t **editor,
void **edit_baton,
apr_bucket_brigade *bb,
dav_svn__output *output,
int compression_level,
int svndiff_version,
apr_pool_t *pool)
{
edit_baton_t *eb = apr_pcalloc(pool, sizeof(*eb));
svn_delta_editor_t *e = svn_delta_default_editor(pool);
eb->bb = bb;
eb->output = output;
eb->started = FALSE;
eb->sending_textdelta = FALSE;
eb->compression_level = compression_level;
eb->svndiff_version = svndiff_version;
e->set_target_revision = set_target_revision;
e->open_root = open_root;
e->delete_entry = delete_entry;
e->add_directory = add_directory;
e->open_directory = open_directory;
e->change_dir_prop = change_dir_prop;
e->add_file = add_file;
e->open_file = open_file;
e->apply_textdelta = apply_textdelta;
e->change_file_prop = change_file_prop;
e->close_file = close_file;
e->close_directory = close_directory;
*editor = e;
*edit_baton = eb;
}
static dav_error *
malformed_element_error(const char *tagname, apr_pool_t *pool)
{
return dav_svn__new_error_svn(pool, HTTP_BAD_REQUEST, 0, 0,
apr_pstrcat(pool,
"The request's '", tagname,
"' element is malformed; there "
"is a problem with the client.",
SVN_VA_NULL));
}
dav_error *
dav_svn__replay_report(const dav_resource *resource,
const apr_xml_doc *doc,
dav_svn__output *output)
{
dav_error *derr = NULL;
svn_revnum_t low_water_mark = SVN_INVALID_REVNUM;
svn_revnum_t rev;
const svn_delta_editor_t *editor;
svn_boolean_t send_deltas = TRUE;
dav_svn__authz_read_baton arb;
const char *base_dir;
apr_bucket_brigade *bb;
apr_xml_elem *child;
svn_fs_root_t *root;
svn_error_t *err;
void *edit_baton;
int ns;
/* In Subversion 1.8, we allowed this REPORT to be issue against a
revision resource. Doing so means the REV is part of the request
URL, and BASE_DIR is embedded in the request body.
The old-school (and incorrect, see issue #4287 --
https://issues.apache.org/jira/browse/SVN-4287) way was
to REPORT on the public URL of the BASE_DIR and embed the REV in
the report body.
*/
if (resource->baselined
&& (resource->type == DAV_RESOURCE_TYPE_VERSION))
{
rev = resource->info->root.rev;
base_dir = NULL;
}
else
{
rev = SVN_INVALID_REVNUM;
base_dir = resource->info->repos_path;
}
arb.r = resource->info->r;
arb.repos = resource->info->repos;
ns = dav_svn__find_ns(doc->namespaces, SVN_XML_NAMESPACE);
if (ns == -1)
return dav_svn__new_error_svn(resource->pool, HTTP_BAD_REQUEST, 0, 0,
"The request does not contain the 'svn:' "
"namespace, so it is not going to have an "
"svn:revision element. That element is "
"required");
for (child = doc->root->first_child; child != NULL; child = child->next)
{
if (child->ns == ns)
{
const char *cdata;
if (strcmp(child->name, "revision") == 0)
{
if (SVN_IS_VALID_REVNUM(rev))
{
/* Uh... we already have a revision to use, either
because this tag is non-unique or because the
request was submitted against a revision-bearing
resource URL. Either way, something's not
right. */
return malformed_element_error("revision", resource->pool);
}
cdata = dav_xml_get_cdata(child, resource->pool, 1);
rev = SVN_STR_TO_REV(cdata);
}
else if (strcmp(child->name, "low-water-mark") == 0)
{
cdata = dav_xml_get_cdata(child, resource->pool, 1);
if (! cdata)
return malformed_element_error("low-water-mark",
resource->pool);
low_water_mark = SVN_STR_TO_REV(cdata);
}
else if (strcmp(child->name, "send-deltas") == 0)
{
apr_int64_t parsed_val;
cdata = dav_xml_get_cdata(child, resource->pool, 1);
if (! cdata)
return malformed_element_error("send-deltas", resource->pool);
err = svn_cstring_strtoi64(&parsed_val, cdata, 0, 1, 10);
if (err)
{
svn_error_clear(err);
return malformed_element_error("send-deltas", resource->pool);
}
send_deltas = parsed_val != 0;
}
else if (strcmp(child->name, "include-path") == 0)
{
cdata = dav_xml_get_cdata(child, resource->pool, 1);
if ((derr = dav_svn__test_canonical(cdata, resource->pool)))
return derr;
/* Force BASE_DIR to be a relative path, not an fspath. */
base_dir = svn_relpath_canonicalize(cdata, resource->pool);
}
}
}
if (! SVN_IS_VALID_REVNUM(rev))
return dav_svn__new_error_svn
(resource->pool, HTTP_BAD_REQUEST, 0, 0,
"Request was missing the revision argument");
if (! SVN_IS_VALID_REVNUM(low_water_mark))
return dav_svn__new_error_svn
(resource->pool, HTTP_BAD_REQUEST, 0, 0,
"Request was missing the low-water-mark argument");
if (! base_dir)
base_dir = "";
bb = apr_brigade_create(resource->pool,
dav_svn__output_get_bucket_alloc(output));
if ((err = svn_fs_revision_root(&root, resource->info->repos->fs, rev,
resource->pool)))
{
derr = dav_svn__convert_err(err, HTTP_INTERNAL_SERVER_ERROR,
"Couldn't retrieve revision root",
resource->pool);
goto cleanup;
}
make_editor(&editor, &edit_baton, bb, output,
dav_svn__get_compression_level(resource->info->r),
resource->info->svndiff_version,
resource->pool);
if ((err = svn_repos_replay2(root, base_dir, low_water_mark,
send_deltas, editor, edit_baton,
dav_svn__authz_read_func(&arb), &arb,
resource->pool)))
{
derr = dav_svn__convert_err(err, HTTP_INTERNAL_SERVER_ERROR,
"Problem replaying revision",
resource->pool);
goto cleanup;
}
if ((err = end_report(edit_baton)))
{
derr = dav_svn__convert_err(err, HTTP_INTERNAL_SERVER_ERROR,
"Problem closing editor drive",
resource->pool);
goto cleanup;
}
cleanup:
dav_svn__operational_log(resource->info,
svn_log__replay(base_dir, rev,
resource->info->r->pool));
/* Flush the brigade. */
return dav_svn__final_flush_or_error(resource->info->r, bb, output,
derr, resource->pool);
}