Author: jpeck
Date: Sat Aug 15 21:26:59 2026
New Revision: 1937142

Log:
Follow-up to r1936010: close the remaining issue #3445 residual in the 
write-through proxy.

The body-rewrite filters still did a blind substitution of the location root,
so a dead-property value containing the master location, or a mid-path directory
named like the slave location, was rewritten along with the genuine hrefs.
Anchor the match on the href tag (<D:href>): serf and mod_dav(_svn) both emit
that prefix with the path immediately after the tag, so only href-initial roots
are translated. A raw <D:href> inside a property value would still be
rewritten;, that cannot currently happen because the emitters escape or
base64-encode values. Although that is not a structural guarantee.

* subversion/mod_dav_svn/mirror.c 
  (PROXY_HREF_ANCHOR): New. Document the href-tag anchor. 
  (locate_ctx_t): Store search/replace strings instead of named local/remote 
paths. 
  (locate_ctx_init): New. Build the anchored pattern and replacement from a
   from-root and a to-root. 
  (dav_svn__location_in_filter, dav_svn__location_body_filter): Use 
locate_ctx_init.
  (dav_svn__location_header_filter): Concatenate the slave root and the 
   remainder without a separator, avoiding a doubled slash.

* subversion/tests/cmdline/dav-mirror-autocheck.sh
  (curl_auth, create_master_txn, delete_master_txn, propfind_slave_txr): New 
   helpers for the in-txn curl probes.
   Add full coverage of all routes through the proxy: move and v1 
MKACTIVITY/CHECKOUT
   including a clean Location rewrite. A proxied revprop change whose value 
contains
   the slave URL, dead-property values surviving a proxied PROPFIND (the former 
XFAIL),
   and a location-name collision in multistatus hrefs. Tests now all include 
<D:href> 
   with the slave URL in the user data to ensure they are escaped properly or
   protected by the filter whitelist.

Modified:
   subversion/branches/proxy-move-copy-fix/subversion/mod_dav_svn/mirror.c
   
subversion/branches/proxy-move-copy-fix/subversion/tests/cmdline/dav-mirror-autocheck.sh

Modified: 
subversion/branches/proxy-move-copy-fix/subversion/mod_dav_svn/mirror.c
==============================================================================
--- subversion/branches/proxy-move-copy-fix/subversion/mod_dav_svn/mirror.c     
Sat Aug 15 13:39:08 2026        (r1937141)
+++ subversion/branches/proxy-move-copy-fix/subversion/mod_dav_svn/mirror.c     
Sat Aug 15 21:26:59 2026        (r1937142)
@@ -32,6 +32,18 @@
 
 #include "dav_svn.h"
 
+/* The rewrite anchor: hrefs in the protocol bodies we filter always
+   appear as <D:href>/path -- serf and mod_dav(_svn) both hard-code the
+   "D:" prefix and emit the value immediately after the tag.  Anchoring
+   the match here means only href-initial location roots are translated;
+   mid-path components and property values pass through.
+
+   A raw "<D:href>" inside a property value would still be rewritten.
+   That cannot currently happen because the emitters escape or
+   base64-encode values (deadprops.c). Although this is not a structural
+   guarantee. */
+#define PROXY_HREF_ANCHOR "<D:href>"
+
 
 /* If the request carries a Destination header (as COPY and MOVE do), rewrite
    it to target the master server instead of this slave. MASTER_URI is the
@@ -207,12 +219,29 @@ typedef struct locate_ctx_t
 {
     const apr_strmatch_pattern *pattern;
     apr_size_t pattern_len;
-    const char *localpath;
-    apr_size_t  localpath_len;
-    const char *remotepath;
-    apr_size_t  remotepath_len;
+    const char *replacement;
+    apr_size_t replacement_len;
 } locate_ctx_t;
 
+/* Initialize CTX to rewrite href-initial FROM_ROOT into TO_ROOT.
+   Both roots are already canonical and URI-encoded, the same domain
+   the protocol bodies use on the wire. */
+static void
+locate_ctx_init(locate_ctx_t *ctx,
+                apr_pool_t *pool,
+                const char *from_root,
+                const char *to_root)
+{
+    const char *from;
+
+    from = apr_pstrcat(pool, PROXY_HREF_ANCHOR, from_root, SVN_VA_NULL);
+    ctx->replacement = apr_pstrcat(pool, PROXY_HREF_ANCHOR, to_root,
+                                   SVN_VA_NULL);
+    ctx->replacement_len = strlen(ctx->replacement);
+    ctx->pattern = apr_strmatch_precompile(pool, from, 1);
+    ctx->pattern_len = strlen(from);
+}
+
 apr_status_t dav_svn__location_in_filter(ap_filter_t *f,
                                          apr_bucket_brigade *bb,
                                          ap_input_mode_t mode,
@@ -244,18 +273,9 @@ apr_status_t dav_svn__location_in_filter
         return ap_get_brigade(f->next, bb, mode, block, readbytes);
     }
 
-    /* Both CANONICALIZED_URI and ROOT_DIR are already canonical and
-       URI-encoded (svn_urlpath__canonicalize() output and the stored
-       <Location> path, respectively), which is the same domain the
-       protocol bodies use on the wire. */
     if (!f->ctx) {
         ctx = f->ctx = apr_pcalloc(r->pool, sizeof(*ctx));
-        ctx->remotepath = canonicalized_uri;
-        ctx->remotepath_len = strlen(ctx->remotepath);
-        ctx->localpath = root_dir;
-        ctx->localpath_len = strlen(ctx->localpath);
-        ctx->pattern = apr_strmatch_precompile(r->pool, ctx->localpath, 1);
-        ctx->pattern_len = ctx->localpath_len;
+        locate_ctx_init(ctx, r->pool, root_dir, canonicalized_uri);
     }
 
     rv = ap_get_brigade(f->next, bb, mode, block, readbytes);
@@ -284,8 +304,8 @@ apr_status_t dav_svn__location_in_filter
             apr_bucket_split(next_bucket, ctx->pattern_len);
             bkt = APR_BUCKET_NEXT(next_bucket);
             apr_bucket_delete(next_bucket);
-            next_bucket = apr_bucket_pool_create(ctx->remotepath,
-                                                 ctx->remotepath_len,
+            next_bucket = apr_bucket_pool_create(ctx->replacement,
+                                                 ctx->replacement_len,
                                                  r->pool, bb->bucket_alloc);
             APR_BUCKET_INSERT_BEFORE(bkt, next_bucket);
         }
@@ -301,7 +321,7 @@ apr_status_t dav_svn__location_header_fi
 {
     request_rec *r = f->r;
     const char *master_uri;
-    const char *location, *start_foo = NULL;
+    const char *location, *remainder = NULL;
 
     /* Don't filter if we're in a subrequest or we aren't setup to
        proxy anything. */
@@ -313,15 +333,18 @@ apr_status_t dav_svn__location_header_fi
 
     location = apr_table_get(r->headers_out, "Location");
     if (location) {
-        start_foo = ap_strstr_c(location, master_uri);
+        remainder = ap_strstr_c(location, master_uri);
     }
-    if (start_foo) {
+    if (remainder) {
         const char *new_uri;
-        start_foo += strlen(master_uri);
+        remainder += strlen(master_uri);
+        /* REMAINDER is empty or begins with '/' (the stored master URI
+           is canonical, with no trailing slash), so concatenate
+           without a separator. */
         new_uri = ap_construct_url(r->pool,
                                    apr_pstrcat(r->pool,
-                                               dav_svn__get_root_dir(r), "/",
-                                               start_foo, SVN_VA_NULL),
+                                               dav_svn__get_root_dir(r),
+                                               remainder, SVN_VA_NULL),
                                    r);
         apr_table_set(r->headers_out, "Location", new_uri);
     }
@@ -379,25 +402,9 @@ apr_status_t dav_svn__location_body_filt
         return ap_pass_brigade(f->next, bb);
     }
 
-    /* ### FIXME (SVN-3445, residual): a PROPFIND multistatus is still 
rewritten
-       ### wholesale below, so a dead-property *value* that happens to contain
-       ### the master location gets silently rewritten along with the genuine
-       ### <D:href>s.  Fixing that safely requires an XML-structure-aware
-       ### rewrite (translate hrefs only, leave property values alone) rather
-       ### than the blind byte substitution used here. */
-
-    /* Both CANONICALIZED_URI and ROOT_DIR are already canonical and
-       URI-encoded (svn_urlpath__canonicalize() output and the stored
-       <Location> path, respectively), which is the same domain the
-       protocol bodies use on the wire. */
     if (!f->ctx) {
         ctx = f->ctx = apr_pcalloc(r->pool, sizeof(*ctx));
-        ctx->remotepath = canonicalized_uri;
-        ctx->remotepath_len = strlen(ctx->remotepath);
-        ctx->localpath = root_dir;
-        ctx->localpath_len = strlen(ctx->localpath);
-        ctx->pattern = apr_strmatch_precompile(r->pool, ctx->remotepath, 1);
-        ctx->pattern_len = ctx->remotepath_len;
+        locate_ctx_init(ctx, r->pool, canonicalized_uri, root_dir);
     }
 
     bkt = APR_BRIGADE_FIRST(bb);
@@ -416,8 +423,8 @@ apr_status_t dav_svn__location_body_filt
             apr_bucket_split(next_bucket, ctx->pattern_len);
             bkt = APR_BUCKET_NEXT(next_bucket);
             apr_bucket_delete(next_bucket);
-            next_bucket = apr_bucket_pool_create(ctx->localpath,
-                                                 ctx->localpath_len,
+            next_bucket = apr_bucket_pool_create(ctx->replacement,
+                                                 ctx->replacement_len,
                                                  r->pool, bb->bucket_alloc);
             APR_BUCKET_INSERT_BEFORE(bkt, next_bucket);
         }

Modified: 
subversion/branches/proxy-move-copy-fix/subversion/tests/cmdline/dav-mirror-autocheck.sh
==============================================================================
--- 
subversion/branches/proxy-move-copy-fix/subversion/tests/cmdline/dav-mirror-autocheck.sh
    Sat Aug 15 13:39:08 2026        (r1937141)
+++ 
subversion/branches/proxy-move-copy-fix/subversion/tests/cmdline/dav-mirror-autocheck.sh
    Sat Aug 15 21:26:59 2026        (r1937142)
@@ -31,10 +31,13 @@
 #
 # The changes sent through the system started as the reproduction
 # recipe for issue 2939 (https://issues.apache.org/jira/browse/SVN-2939,
-# using svnmucc) and have grown to cover URI-encoded locations, the
-# SVN-3445 payload-corruption regressions, proxied reads of transaction
-# resources, and locks.  Any svn traffic liable to break over
-# mirroring remains a good addition.
+# using svnmucc) and have grown to cover URI-encoded locations,
+# COPY/MOVE Destination rewriting, the SVN-3445 payload-corruption
+# regressions, proxied reads of transaction resources (including
+# dead-property values and location-name collisions in rewritten
+# hrefs), HTTPv1 MKACTIVITY/CHECKOUT, revision-property changes
+# through the proxy, and locks.  Any svn traffic liable to break
+# over mirroring remains a good addition.
 #
 # Most of the httpd setup was lifted from davautocheck.sh.
 # The common boilerplate snippets to setup/start/stop httpd
@@ -64,6 +67,45 @@ function fail() {
   stop_httpd_and_die
 }
 
+# Authenticated curl against the test repositories.  CURL is resolved
+# later; the functions below expand it at call time.
+function curl_auth() {
+  $CURL --silent --show-error --user jrandom:rayjandom "$@"
+}
+
+# Open a transaction on the master (raw HTTPv2 create-txn POST) and print
+# its SVN-Txn-Name.  Used by probes that must read in-txn data through the
+# slave; committed reads are served locally and never hit the rewrite.
+function create_master_txn() {
+  curl_auth \
+    --request POST \
+    --header "Content-Type: application/vnd.svn-skel" --data "( create-txn )" \
+    --dump-header - --output /dev/null "$MASTER_URL/!svn/me" \
+    | sed -ne 's/^SVN-Txn-Name: *//p' | tr -d '\r'
+}
+
+function delete_master_txn() {
+  curl_auth --request DELETE --output /dev/null \
+    "$MASTER_URL/!svn/txn/$1" \
+    || say "WARNING: could not delete test txn $1 (continuing)" >&2
+}
+
+# PROPFIND PATH in a fresh master txn through the slave.  Stores the
+# multistatus in DEST (a variable name) and leaves TXN_NAME set so
+# callers can assert on the txn id in hrefs.  WHAT is a short label
+# for the fail message.  Assigns in this shell so TXN_NAME survives
+# (command substitution would lose it).
+function propfind_slave_txr() {
+  local dest="$1"
+  local path="$2"
+  local what="$3"
+  TXN_NAME=$(create_master_txn)
+  [ -n "$TXN_NAME" ] || fail "could not create a txn on the master${what:+ 
($what)}"
+  printf -v "$dest" '%s' "$(curl_auth --request PROPFIND --header "Depth: 0" \
+    "$SLAVE_URL/!svn/txr/$TXN_NAME/$path")"
+  delete_master_txn "$TXN_NAME"
+}
+
 function get_loadmodule_config() {
   local SO="$($APXS -q LIBEXECDIR)/$1.so"
 
@@ -388,6 +430,12 @@ MASTER_URL="http://${MASTER_HOST}:${TEST
 SLAVE_URL="http://${SLAVE_HOST}:${TEST_PORT}/${SLAVE_LOCATION_URI}";
 SYNC_URL="http://${SLAVE_HOST}:${TEST_PORT}/${SYNC_LOCATION}";
 
+# User-data payloads that include the rewrite-anchor tag plus a location
+# root -- the byte sequence the body filter matches.  Protocol XML
+# escapes this; file content and skels carry it raw.
+HREF_IN_MASTER="<D:href>/${MASTER_LOCATION_URI}"
+HREF_IN_SLAVE="<D:href>/${SLAVE_LOCATION_URI}"
+
 BASE_URL="$SLAVE_URL"
 
 # setup server and repositories
@@ -412,9 +460,11 @@ read SLAVE_UUID < "$SLAVE_REPOS/db/uuid"
 [ "$SLAVE_UUID" = "$MASTER_UUID" ] \
   || fail "master/slave uuid mismatch"
 # setup hooks:
-#  slave allows revprop changes
+#  slave and master allow revprop changes (the latter so a proxied
+#  svn propset --revprop can succeed)
 #  master syncs changes to slave
 echo "#!/bin/sh" > "$SLAVE_REPOS/hooks/pre-revprop-change"
+echo "#!/bin/sh" > "$MASTER_REPOS/hooks/pre-revprop-change"
 echo "#!/bin/sh" > "$MASTER_REPOS/hooks/post-revprop-change"
 echo "#!/bin/sh" > "$MASTER_REPOS/hooks/post-commit"
 echo "$SVNSYNC --non-interactive sync '$SYNC_URL' --username=svnsync 
--password=svnsync" \
@@ -423,6 +473,7 @@ echo "$SVNSYNC --non-interactive sync '$
     >> "$MASTER_REPOS/hooks/post-commit"
 
 chmod 0755 "$SLAVE_REPOS/hooks/pre-revprop-change"
+chmod 0755 "$MASTER_REPOS/hooks/pre-revprop-change"
 chmod 0755 "$MASTER_REPOS/hooks/post-revprop-change"
 chmod 0755 "$MASTER_REPOS/hooks/post-commit"
 
@@ -506,6 +557,21 @@ $SVNLOOK tree --full-paths "$MASTER_REPO
 
 say "PASS: committing a path which has a space in it passes"
 
+# An explicit move is a COPY plus a DELETE of the source within one txn.
+# The Destination rewrite is the same path as the space-in-name COPY
+# above; this checks the pair lands as a move on the master.
+say "Test case for move (COPY + DELETE) through the proxy"
+
+$svnmucc mkdir "$BASE_URL/move-src" \
+  || fail "creating move source failed"
+$svnmucc mv "$BASE_URL/move-src" "$BASE_URL/move-src-moved" \
+  || fail "move through the proxy failed"
+$SVNLOOK tree --full-paths "$MASTER_REPOS" | grep -Fq "move-src-moved/" \
+  || fail "moved directory missing on the master"
+$SVNLOOK tree --full-paths "$MASTER_REPOS" | grep -Fq "move-src/" \
+  && fail "move source still present on the master"
+say "PASS: move (copy + delete) works through the proxy"
+
 # Regression coverage for SVN-3445.  When the master and slave locations
 # differ, the proxy must translate the location prefix in *protocol* URLs
 # (hrefs in MERGE/CHECKOUT bodies) but must NOT touch *user payload*:
@@ -513,30 +579,30 @@ say "PASS: committing a path which has a
 # PROPPATCH bodies, and log-message/revprop skels in POST bodies.
 say "Test case for versioned content munging (SVN-3445)"
 
-# File content must round-trip verbatim whether it embeds the master URL
-# or the slave URL (used to be mangled/rejected).
-echo "$MASTER_URL" > "$HTTPD_ROOT/master-url.txt"
-echo "$SLAVE_URL"  > "$HTTPD_ROOT/slave-url.txt"
+# File content must round-trip verbatim whether it embeds the master or
+# slave rewrite-anchor sequence (used to be mangled/rejected).
+echo "$HREF_IN_MASTER" > "$HTTPD_ROOT/master-url.txt"
+echo "$HREF_IN_SLAVE"  > "$HTTPD_ROOT/slave-url.txt"
 $svnmucc put "$HTTPD_ROOT/master-url.txt" "$BASE_URL/master-url.txt" \
          put "$HTTPD_ROOT/slave-url.txt"  "$BASE_URL/slave-url.txt" \
-  || fail "committing URL-bearing files failed (SVN-3445: PUT body rewritten?)"
+  || fail "committing href-bearing files failed (SVN-3445: PUT body 
rewritten?)"
 
 master_file_content=$($SVNLOOK cat "$SLAVE_REPOS" master-url.txt)
-[ "$master_file_content" = "$MASTER_URL" ] \
-  || fail "file content embedding the master URL was munged: committed 
'$MASTER_URL', slave stores '$master_file_content'"
+[ "$master_file_content" = "$HREF_IN_MASTER" ] \
+  || fail "file content embedding the master href tag was munged: committed 
'$HREF_IN_MASTER', slave stores '$master_file_content'"
 slave_file_content=$($SVNLOOK cat "$SLAVE_REPOS" slave-url.txt)
-[ "$slave_file_content" = "$SLAVE_URL" ] \
-  || fail "file content embedding the slave URL was munged: committed 
'$SLAVE_URL', slave stores '$slave_file_content'"
-say "PASS: file content is preserved verbatim regardless of embedded URL"
+[ "$slave_file_content" = "$HREF_IN_SLAVE" ] \
+  || fail "file content embedding the slave href tag was munged: committed 
'$HREF_IN_SLAVE', slave stores '$slave_file_content'"
+say "PASS: file content is preserved verbatim regardless of embedded href tag"
 
 # Property values must likewise round-trip verbatim: a PROPPATCH value that
-# contains the slave URL used to be silently rewritten.
-$svnmucc propset svn-3445-prop "$SLAVE_URL" "$BASE_URL/slave-url.txt" \
-  || fail "propset of a value containing the slave URL failed"
+# contains the rewrite-anchor sequence used to be silently rewritten.
+$svnmucc propset svn-3445-prop "$HREF_IN_SLAVE" "$BASE_URL/slave-url.txt" \
+  || fail "propset of a value containing the slave href tag failed"
 prop_value=$($SVNLOOK propget "$SLAVE_REPOS" svn-3445-prop slave-url.txt)
-[ "$prop_value" = "$SLAVE_URL" ] \
-  || fail "property value embedding the slave URL was munged: set 
'$SLAVE_URL', slave stores '$prop_value'"
-say "PASS: property value is preserved verbatim regardless of embedded URL"
+[ "$prop_value" = "$HREF_IN_SLAVE" ] \
+  || fail "property value embedding the slave href tag was munged: set 
'$HREF_IN_SLAVE', slave stores '$prop_value'"
+say "PASS: property value is preserved verbatim regardless of embedded href 
tag"
 
 # Commit log messages and revision properties travel inside the
 # create-txn-with-props POST body (HTTPv2), a length-prefixed skel
@@ -544,24 +610,35 @@ say "PASS: property value is preserved v
 # corrupts the values, and when the location paths differ in length it
 # breaks the skel framing outright.  Both must round-trip verbatim.
 # (The $svnmucc wrapper bakes in -mm, so invoke $SVNMUCC directly.)
-log_msg="log mentioning the slave URL: $SLAVE_URL"
+log_msg="log mentioning the slave href tag: $HREF_IN_SLAVE"
 $SVNMUCC --non-interactive --username jrandom --password rayjandom \
-         -m "$log_msg" --with-revprop "svn-3445-revprop=$SLAVE_URL" \
+         -m "$log_msg" --with-revprop "svn-3445-revprop=$HREF_IN_SLAVE" \
          mkdir "$BASE_URL/log-url-dir" \
-  || fail "commit with a log message containing the slave URL failed (POST 
body rewritten?)"
+  || fail "commit with a log message containing the slave href tag failed 
(POST body rewritten?)"
 rev=$($SVNLOOK youngest "$SLAVE_REPOS")
 stored_log=$($SVNLOOK propget --revprop -r "$rev" "$SLAVE_REPOS" svn:log)
 [ "$stored_log" = "$log_msg" ] \
   || fail "log message was munged: committed '$log_msg', slave stores 
'$stored_log'"
 stored_revprop=$($SVNLOOK propget --revprop -r "$rev" "$SLAVE_REPOS" 
svn-3445-revprop)
-[ "$stored_revprop" = "$SLAVE_URL" ] \
-  || fail "revprop value was munged: set '$SLAVE_URL', slave stores 
'$stored_revprop'"
+[ "$stored_revprop" = "$HREF_IN_SLAVE" ] \
+  || fail "revprop value was munged: set '$HREF_IN_SLAVE', slave stores 
'$stored_revprop'"
 say "PASS: log message and revprop values are preserved verbatim"
 
-# The response side (mirror.c attaches the body-rewrite filter only for
-# MERGE and PROPFIND, with the response_is_xml() gate as a backstop) is
-# covered by the proxied-txn-reads test below.  The residual block further
-# down exercises the one remaining SVN-3445 residual.
+# PROPPATCH on !svn/rev is a different path from the create-txn-with-props
+# POST above: the body is an opaque property value and must stay out of
+# the rewrite.  The master's pre-revprop-change hook is enabled above
+# so this can succeed.
+say "Test case for revision-property change through the proxy"
+
+rev=$($SVNLOOK youngest "$MASTER_REPOS")
+$SVN propset --revprop -r "$rev" --non-interactive \
+     --username jrandom --password rayjandom \
+     test:revprop "url is $HREF_IN_SLAVE" "$SLAVE_URL" \
+  || fail "revprop change through the proxy failed"
+stored_rp=$($SVNLOOK propget --revprop -r "$rev" "$MASTER_REPOS" test:revprop)
+[ "$stored_rp" = "url is $HREF_IN_SLAVE" ] \
+  || fail "revprop value was munged through the proxy: stored '$stored_rp'"
+say "PASS: revision property change through the proxy is stored verbatim"
 
 # Response-side SVN-3445 coverage: reads of transaction resources are
 # proxied to the master; the proxy must rewrite hrefs in protocol XML
@@ -575,101 +652,116 @@ say "PASS: log message and revprop value
 # without ever traversing the proxy or the response filter under test.
 say "Test case for proxied reads of txn resources (SVN-3445 response side)"
 
-curl_auth="$CURL --silent --show-error --user jrandom:rayjandom"
-
-# Commit an XML-mime-typed file for probe 3 below (before the txn is
+# Commit an XML-mime-typed file for the GET probe below (before the txn is
 # opened, so the txn tree contains it).
-printf '<?xml version="1.0"?>\n<note><!-- %s --></note>\n' "$MASTER_URL" \
+printf '<?xml version="1.0"?>\n<note><!-- %s --></note>\n' "$HREF_IN_MASTER" \
   > "$HTTPD_ROOT/xml-payload.xml"
 $svnmucc put "$HTTPD_ROOT/xml-payload.xml" "$BASE_URL/xml-payload.xml" \
          propset svn:mime-type text/xml "$BASE_URL/xml-payload.xml" \
   || fail "committing the XML-typed payload file failed"
 
-# Open a transaction directly on the master (raw HTTPv2 create-txn POST)
-# and harvest its name from the SVN-Txn-Name response header.
-txn_name=$($curl_auth --request POST \
-  --header "Content-Type: application/vnd.svn-skel" --data "( create-txn )" \
-  --dump-header - --output /dev/null "$MASTER_URL/!svn/me" \
-  | sed -ne 's/^SVN-Txn-Name: *//p' | tr -d '\r')
+txn_name=$(create_master_txn)
 [ -n "$txn_name" ] || fail "could not create a txn on the master"
 
-# 1. Non-XML payload: GET the file through the SLAVE. The response is
-#    proxied from the master and must arrive verbatim, the slave URL
-#    embedded in the content must NOT have been rewritten.
-proxied_get=$($curl_auth "$SLAVE_URL/!svn/txr/$txn_name/slave-url.txt")
-[ "$proxied_get" = "$SLAVE_URL" ] \
-  || fail "proxied GET of txn file content was munged: expected '$SLAVE_URL', 
got '$proxied_get'"
-
-# 2. Protocol XML: PROPFIND on the txn file through the SLAVE. The
-#    multistatus hrefs come from the master and MUST be rewritten to the
-#    slave location.
-proxied_propfind=$($curl_auth --request PROPFIND --header "Depth: 0" \
-  "$SLAVE_URL/!svn/txr/$txn_name/slave-url.txt")
+# Non-XML payload: GET the file through the SLAVE. The response is
+# proxied from the master and must arrive verbatim, the slave href
+# tag embedded in the content must NOT have been rewritten.
+proxied_get=$(curl_auth "$SLAVE_URL/!svn/txr/$txn_name/slave-url.txt")
+[ "$proxied_get" = "$HREF_IN_SLAVE" ] \
+  || fail "proxied GET of txn file content was munged: expected 
'$HREF_IN_SLAVE', got '$proxied_get'"
+
+# Regression guard: a versioned file whose svn:mime-type is XML must
+# never have its content rewritten on a proxied read.  Doubly protected:
+# GET responses do not receive the ReposRewrite body filter at all
+# (proxy_request_fixup() attaches it only for MERGE and PROPFIND), and
+# mod_dav_svn happens to emit no Content-Type for !svn/txr file GETs.
+proxied_xml_get=$(curl_auth "$SLAVE_URL/!svn/txr/$txn_name/xml-payload.xml")
+echo "$proxied_xml_get" | grep -qF "$HREF_IN_MASTER" \
+  || fail "XML-typed txn file content did not round-trip a proxied read 
verbatim: got '$proxied_xml_get'"
+delete_master_txn "$txn_name"
+say "PASS: XML-typed txn file content survives a proxied read verbatim"
+
+# Protocol XML: PROPFIND on the txn file through the SLAVE. The
+# multistatus hrefs come from the master and MUST be rewritten to the
+# slave location.
+propfind_slave_txr proxied_propfind "slave-url.txt" "proxied reads"
 echo "$proxied_propfind" | grep -qF "/${SLAVE_LOCATION_URI}/" \
   || fail "proxied PROPFIND multistatus hrefs were not rewritten to the slave 
location: $proxied_propfind"
 echo "$proxied_propfind" | grep -qF "/${MASTER_LOCATION_URI}/" \
   && fail "proxied PROPFIND multistatus still contains master-location hrefs: 
$proxied_propfind"
 
-# 3. Regression guard: a versioned file whose svn:mime-type is XML must
-#    never have its content rewritten on a proxied read.  Doubly protected:
-#    GET responses do not receive the ReposRewrite body filter at all
-#    (proxy_request_fixup() attaches it only for MERGE and PROPFIND), and
-#    mod_dav_svn happens to emit no Content-Type for !svn/txr file GETs.
-proxied_xml_get=$($curl_auth "$SLAVE_URL/!svn/txr/$txn_name/xml-payload.xml")
-echo "$proxied_xml_get" | grep -qF "$MASTER_URL" \
-  || fail "XML-typed txn file content did not round-trip a proxied read 
verbatim: got '$proxied_xml_get'"
-say "PASS: XML-typed txn file content survives a proxied read verbatim"
-
-# Clean up the open txn so later consistency checks aren't confused.
-$curl_auth --request DELETE --output /dev/null \
-  "$MASTER_URL/!svn/txn/$txn_name" \
-  || say "WARNING: could not delete test txn $txn_name (continuing)"
-
 say "PASS: proxied txn reads: content verbatim, protocol hrefs rewritten"
 
-# The one remaining SVN-3445 residual: a dead-property value rewritten in
-# a proxied PROPFIND multistatus.  Needs a proxied read of in-transaction
-# data (reads of committed data are served locally by the slave and never
-# traverse the response filter), so it reuses the curl txn-probe technique
-# from the test above.
-say "Test case for the SVN-3445 response-side residual (XFAIL expected)"
-
-# The munged fingerprint: a master-URL value rewritten by the response
-# filter keeps the master host but gains the slave location path.
-master_url_munged="http://${MASTER_HOST}:${TEST_PORT}/${SLAVE_LOCATION_URI}";
-
-# The residual: a dead-property VALUE containing the master URL, returned
-# inside a proxied PROPFIND multistatus, is blindly rewritten along with
-# the genuine hrefs (see the "FIXME (SVN-3445, residual)" comment in
-# mirror.c's dav_svn__location_body_filter).
-$svnmucc propset svn-3445-residual-prop "$MASTER_URL" 
"$BASE_URL/master-url.txt" \
-  || fail "propset of a master-URL value failed"
-
-# Open a fresh txn on the master (after the propset above, so its tree
-# contains the property).
-txn_name=$($curl_auth --request POST \
-  --header "Content-Type: application/vnd.svn-skel" --data "( create-txn )" \
-  --dump-header - --output /dev/null "$MASTER_URL/!svn/me" \
-  | sed -ne 's/^SVN-Txn-Name: *//p' | tr -d '\r')
-[ -n "$txn_name" ] || fail "could not create a txn on the master (residual 
tests)"
-
-# Residual probe: PROPFIND (allprop) on the file through the SLAVE.
-residual_propfind=$($curl_auth --request PROPFIND --header "Depth: 0" \
-  "$SLAVE_URL/!svn/txr/$txn_name/master-url.txt")
-if echo "$residual_propfind" | grep -qF "$MASTER_URL"; then
-  say "XPASS: SVN-3445 residual appears fixed: dead-property value survived a"
-  say "       proxied PROPFIND verbatim."
-elif echo "$residual_propfind" | grep -qF "$master_url_munged"; then
-  say "XFAIL (SVN-3445 residual): dead-property value was rewritten in the"
-  say "       proxied multistatus: '$MASTER_URL' -> '$master_url_munged'."
-else
-  fail "residual PROPFIND contained neither the original nor the munged value: 
$residual_propfind"
-fi
+# Dead-property values in a proxied PROPFIND must survive verbatim while
+# the surrounding hrefs are translated.  Needs a proxied read of in-txn
+# data (committed reads are served locally and never hit the filter).
+say "Test case for dead-property values in a proxied multistatus"
+
+$svnmucc propset url-bearing-prop "$HREF_IN_MASTER" "$BASE_URL/master-url.txt" 
\
+  || fail "propset of a master href-tag value failed"
+
+propfind_slave_txr deadprop_propfind "master-url.txt" "dead-property test"
+# The value is XML-escaped in the multistatus; the raw tag must not appear
+# as a rewritten protocol href.
+echo "$deadprop_propfind" | grep -qF "&lt;D:href&gt;/${MASTER_LOCATION_URI}" \
+  || fail "dead-property value did not survive the proxied PROPFIND verbatim: 
$deadprop_propfind"
+echo "$deadprop_propfind" | grep -qF "<D:href>/${SLAVE_LOCATION_URI}/" \
+  || fail "multistatus href was not translated to the slave location: 
$deadprop_propfind"
+say "PASS: dead-property values survive proxied multistatus href translation"
+
+# A repos path component named like the master location must survive
+# root translation in proxied multistatus hrefs.
+say "Test case for location-name collision in proxied hrefs"
+
+$svnmucc mkdir "$BASE_URL/${MASTER_LOCATION_URI}" \
+  || fail "committing a directory named after the master location failed"
+$SVNLOOK tree --full-paths "$MASTER_REPOS" | grep -Fq "${MASTER_LOCATION}/" \
+  || fail "directory named after the master location missing on the master"
+
+propfind_slave_txr collision_propfind "${MASTER_LOCATION_URI}" "collision test"
+echo "$collision_propfind" | grep -qF "/$TXN_NAME/${MASTER_LOCATION_URI}" \
+  || fail "href component named after the master location did not survive the 
proxied PROPFIND: $collision_propfind"
+echo "$collision_propfind" | grep -qF "<D:href>/${SLAVE_LOCATION_URI}/" \
+  || fail "collision multistatus href root was not translated to the slave 
location: $collision_propfind"
+say "PASS: href components named after a location survive the anchored rewrite"
+
+# The HTTPv1 commit opening, emulated with curl (no modern client speaks
+# it, but mod_dav_svn still serves it): MKACTIVITY names an activity, and
+# CHECKOUT of a version resource carries that activity's href -- as
+# <D:href> in the request body -- which the request-side filter must
+# translate for the master to resolve it.  The 201 itself proves the body
+# translation (an untranslated activity href cannot resolve on the
+# master); the Location header must come back rewritten to the slave
+# root with no doubled slash; and a PROPFIND of the resulting working
+# resource covers the !svn/wrk/ routing branch.
+say "Test case for v1 commit opening (MKACTIVITY/CHECKOUT) through the proxy"
+
+v1_activity="dav-mirror-v1-activity-$$"
+mka_status=$(curl_auth --request MKACTIVITY -o /dev/null -w "%{http_code}" \
+  "$SLAVE_URL/!svn/act/$v1_activity")
+[ "$mka_status" = "201" ] \
+  || fail "MKACTIVITY through the proxy failed (HTTP $mka_status)"
 
-# Clean up the open txn.
-$curl_auth --request DELETE --output /dev/null \
-  "$MASTER_URL/!svn/txn/$txn_name" \
-  || say "WARNING: could not delete residual-test txn $txn_name (continuing)"
+rev=$($SVNLOOK youngest "$SLAVE_REPOS")
+printf '<?xml version="1.0" encoding="utf-8"?><D:checkout 
xmlns:D="DAV:"><D:activity-set><D:href>/%s/!svn/act/%s</D:href></D:activity-set></D:checkout>'
 \
+  "$SLAVE_LOCATION_URI" "$v1_activity" > "$HTTPD_ROOT/checkout-body.xml"
+v1_location=$(curl_auth --request CHECKOUT --header "Content-Type: text/xml" \
+  --data @"$HTTPD_ROOT/checkout-body.xml" --dump-header - --output /dev/null \
+  "$SLAVE_URL/!svn/ver/$rev/master-url.txt" \
+  | sed -ne 's/^Location: *//p' | tr -d '\r')
+[ -n "$v1_location" ] \
+  || fail "v1 CHECKOUT through the proxy returned no Location header (body 
href untranslated?)"
+echo "$v1_location" | grep -qF 
"$SLAVE_URL/!svn/wrk/$v1_activity/master-url.txt" \
+  || fail "CHECKOUT Location was not rewritten cleanly to the slave root: 
'$v1_location'"
+
+wrk_propfind=$(curl_auth --request PROPFIND --header "Depth: 0" \
+  "$SLAVE_URL/!svn/wrk/$v1_activity/master-url.txt")
+echo "$wrk_propfind" | grep -qF "<D:href>/${SLAVE_LOCATION_URI}/!svn/wrk/" \
+  || fail "working-resource PROPFIND href was not translated to the slave 
location: $wrk_propfind"
+
+curl_auth --request DELETE --output /dev/null 
"$SLAVE_URL/!svn/act/$v1_activity" \
+  || say "WARNING: could not delete v1 test activity (continuing)" >&2
+say "PASS: v1 activity checkout, Location rewrite, and wrk reads work through 
the proxy"
 
 # LOCK/UNLOCK are proxied methods, and locked-file commits push lock tokens
 # through the proxy.  The lock comment travels as the DAV owner element in
@@ -679,7 +771,7 @@ say "Test case for locks through the wri
 svncmd="$SVN --non-interactive --username=jrandom --password=rayjandom"
 $svncmd checkout -q "$BASE_URL" "$HTTPD_ROOT/wc-lock" \
   || fail "checkout for lock test failed"
-$svncmd lock -m "locked via slave: $SLAVE_URL" 
"$HTTPD_ROOT/wc-lock/slave-url.txt" \
+$svncmd lock -m "locked via slave: $HREF_IN_SLAVE" 
"$HTTPD_ROOT/wc-lock/slave-url.txt" \
   || fail "svn lock through the proxy failed"
 
 # The lock must exist on the MASTER (locks are not versioned; svnsync does
@@ -690,8 +782,8 @@ $SVNLOOK lock "$MASTER_REPOS" slave-url.
 # The client's own view of the comment is just the cached copy in its
 # working-copy lock table, so inspect what the master actually stored.
 lock_comment=$($SVNLOOK lock "$MASTER_REPOS" slave-url.txt)
-echo "$lock_comment" | grep -qF "$SLAVE_URL" \
-  || fail "stored lock comment was munged by the proxy: expected it to contain 
'$SLAVE_URL', master stores: $lock_comment"
+echo "$lock_comment" | grep -qF "$HREF_IN_SLAVE" \
+  || fail "stored lock comment was munged by the proxy: expected it to contain 
'$HREF_IN_SLAVE', master stores: $lock_comment"
 say "PASS: lock comment survives the proxy verbatim"
 
 # Committing a change to the locked file sends the lock token with the

Reply via email to