You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@subversion.apache.org by Stefan Sperling <st...@elego.de> on 2010/07/07 20:39:02 UTC

[PATCH] issue #3555: make svn_stream_readline() a stream method

This change was proposed by Julian some time ago, because having
callback types that change the behaviour of svn_stream_readline(),
namely svn_io_line_filter_cb_t and svn_io_line_transformer_cb_t,
was the wrong approach.
See http://subversion.tigris.org/issues/show_bug.cgi?id=3555

The patch below implements Julian's second suggestion from the issue:
"make readline() a virtual method of svn_stream_t and then have the user
provide replacement implementations of readline() for doing the required
filtering."

One caveat is that to remain backwards compatible we cannot require
streams to set a readline method. So svn_stream_readline() falls back
to the default readline implementation if none has been set.

I have taken a shortcut and made svn_stream_readline_detect_eol() (which
is new in 1.7) also provide a readline fallback. This avoids having to
add dummy readline methods to streams elsewhere in our code, such as the
one in fs_fs.c.

svn_stream_readline_detect_eol() now behaves like svn_stream_readline()
if an EOL character is passed in.
I briefly considered deprecating svn_stream_readline() and renaming
svn_stream_readline_detect_eol() to svn_stream_readline2(), but
decided against doing so as it would cause a lot of code churn and
existing callers of svn_stream_readline() won't benefit from the upgrade.

The readline callbacks were initially added to address the needs of svn
patch, in particularly the diff parser. The parser now has two custom readline
implementations. There is some code duplication between implementations.
But the generic readline method becomes a bit simpler again. It's not as
simple as it was in 1.6.x, because we need a way to read lines with arbitrary
EOL characters. The diffstat illustrates nicely how code has shifted around:


 include/svn_io.h                |   97 +++-----
 libsvn_client/patch.c           |   16 -
 libsvn_diff/parse-diff.c        |  452 +++++++++++++++++++++++++++++++++-------
 libsvn_subr/stream.c            |  285 ++++++++++++-------------
 tests/libsvn_subr/stream-test.c |  142 ------------
 5 files changed, 572 insertions(+), 420 deletions(-)

The patch passes make check.
I'm posting it here for review by Julian and others who are interested.

Thanks,
Stefan

[[[
Fix issue #3555, 'Remove the "line_filter" and "line_transformer" callbacks
from svn_stream_t'.

Make svn_stream_readline() a virtual method of svn_stream_t,
and provide custom implementations of readline methods in the diff
parsing code (which is the only place where we need this right now).

* subversion/include/svn_io.h
  (svn_io_line_filter_cb_t, svn_io_line_transformer_cb_t,
   svn_stream_set_line_filter_callback,
   svn_stream_set_line_transformer_callback): Remove.
  (svn_io_readline_fn_t, svn_stream_set_readline): Declare.
  (svn_stream_readline): Adjust docstring.
  (svn_stream_readline_detect_eol): Switch to dual-pool paradigm,
   and document the new twisted behaviour of the EOL input/output parameter.

* subversion/libsvn_diff/parse-diff.c
  (original_line_filter, modified_line_filter, remove_leading_char_transformer,
   reverse_diff_transformer): Remove.
  (hunk_text_stream_baton, read_handler_hunk_text, write_handler_hunk_text,
   close_handler_hunk_text, reset_handler_hunk_text, mark_handler_hunk_text,
   seek_handler_hunk_text, scan_eol, readline_handler_hunk_text,
   stream_hunk_text, stream_hunk_text_original, stream_hunk_text_modified):
    New. Implementation of a stream which provides a special readline
    method to read original/modified texts of hunks from a patch file stream.
  (reverse_diff_text_stream_baton, read_handler_reverse_diff_text,
   write_handler_reverse_diff_text, close_handler_reverse_diff_text,
   reset_handler_reverse_diff_text, mark_handler_reverse_diff_text,
   seek_handler_reverse_diff_text, readline_handler_reverse_diff_text,
   stream_reverse_diff_text): New. Implementation of a stream which
    provides a special readline method which reverses unidiff data read
    from the wrapped stream.
  (parse_next_hunk): Track svn_stream_readline_detect_eol() dual-pool change.
   Add a comment explaining why the patch file gets opened multiple
   times (drive-by fix because this confused me at first).
   Instead of installing line-filter/transformation callbacks on
   streams, wrap streams with appropriate wrapper streams.

* subversion/libsvn_subr/stream.c
  (struct svn_stream_t): Replace line_filter_cb and line_transformer_cb
   members with readline_fn member.
  (svn_stream_create): Track changes to svn_stream_t.
  (svn_stream_set_line_filter_callback,
   svn_stream_set_line_transformer_callback,
   line_filter, line_transformer): Remove.
  (scan_eol): Tweak argument list for use within a stream method.
   This function can no longer expect a stream, so pass a baton
   and a set of required stream methods instead.
  (stream_readline): Make this in svn_io_readline_fn_t implementation.
   Remove handling of line filters/transformers.
  (svn_stream_readline): Instead of calling the stream_readline() helper
   directly, call a custom readline implementation if one is set on the
   stream. If no custom implementation is provided, fall back to the
   stream_readline() helper function to preserve compatibility with 1.6.x.
   Assert that the caller passed an EOL character, because the new
   svn_io_readline_fn_t does not strictly require it.
  (svn_stream_readline_detect_eol): As previous, but ensure that
   the stream has mark/seek support instead of asserting an EOL.
  (readline_handler_empty): Custom readline handler for the empty stream.
  (svn_stream_empty, svn_stream_disown, svn_stream_from_aprfile2,
   svn_stream_from_aprfile_range_readonly, svn_stream_compressed,
   svn_stream_checksummed2, svn_stream_checksummed, svn_stream_from_stringbuf,
   svn_stream_from_string): Set a readline method.

* subversion/libsvn_client/patch.c
  (read_line, match_hunk, reject_hunk, apply_hunk): Ensure that EOL_STR
    is always initialised to fulfill new API requirements of
    svn_stream_readline_detect_eol regarding EOLs.
    Also pass two pools to svn_stream_readline_detect_eol().

* subversion/tests/libsvn_subr/stream-test.c
  (line_filter, test_stream_line_filter, line_transformer,
   test_stream_line_transformer,
   test_stream_line_filter_and_transformer): Remove these tests.
  (test_funcs): Remove removed tests.

]]]


Index: subversion/include/svn_io.h
===================================================================
--- subversion/include/svn_io.h	(revision 961349)
+++ subversion/include/svn_io.h	(working copy)
@@ -780,40 +780,36 @@ typedef svn_error_t *(*svn_io_mark_fn_t)(void *bat
 typedef svn_error_t *(*svn_io_seek_fn_t)(void *baton,
                                          svn_stream_mark_t *mark);
 
-/** Line-filtering callback function for a generic stream.
- * @a baton is the stream's baton.
- * @see svn_stream_t, svn_stream_set_baton() and svn_stream_readline().
+/** Line-reading handler function for a generic stream.
  *
- * @since New in 1.7.
- */
-typedef svn_error_t *(*svn_io_line_filter_cb_t)(svn_boolean_t *filtered,
-                                                const char *line,
-                                                void *baton,
-                                                apr_pool_t *scratch_pool);
-
-/** A callback function, invoked by svn_stream_readline(), which can perform
- * arbitary transformations on the line before it is passed back to the caller
- * of svn_stream_readline().
+ * Allocate @a *stringbuf in @a result_pool, and read into it one line
+ * from @a stream. If @a *eol is not NULL, it is used as the expected
+ * line terminator. If @a eol is NULL, the line-terminator is detected
+ * automatically. If @a *eol is NULL, the line-terminator is detected
+ * automatically and is returned in @a *eol.
  *
- * Returns a transformed stringbuf in @a buf, allocated in @a result_pool.
- * This callback gets invoked on lines which were not filtered by the
- * line-filtering callback function.
+ * @a mark_fn and @a seek_fn are required to be non-NULL if the end-of-line
+ * indicator is to be detected automatically. Else, they may be NULL.
  *
- * Implementations should always at least return an empty stringbuf.
- * It is a fatal error if an implementation returns @a *buf as NULL.
+ * The line-terminator is read from the stream, but is not added to
+ * the end of the stringbuf.  Instead, the stringbuf ends with a usual '\\0'.
  *
- * @a baton is the stream's baton.
+ * If @a stream runs out of bytes before encountering a line-terminator,
+ * then set @a *eof to @c TRUE, otherwise set @a *eof to FALSE.
  *
- * @see svn_stream_t, svn_stream_set_baton(), svn_io_line_filter_cb_t and
- * svn_stream_readline().
+ * Temporary allocations will be performed in @a scratch_pool.
  *
- * @since New in 1.7.
- */
-typedef svn_error_t *(*svn_io_line_transformer_cb_t)(svn_stringbuf_t **buf,
-                                                     const char *line,
-                                                     void *baton,
-                                                     apr_pool_t *result_pool,
-                                                     apr_pool_t *scratch_pool);
+ * @see svn_stream_readline and svn_stream_readline_detect_eol.
+ * @since New in 1.7. */
+typedef svn_error_t *(*svn_io_readline_fn_t)(void *baton,
+                                             svn_read_fn_t read_fn,
+                                             svn_io_mark_fn_t mark_fn,
+                                             svn_io_seek_fn_t seek_fn,
+                                             svn_stringbuf_t **stringbuf,
+                                             const char **eol,
+                                             svn_boolean_t *eof,
+                                             apr_pool_t *result_pool,
+                                             apr_pool_t *scratch_pool);
 
 /** Create a generic stream.  @see svn_stream_t. */
 svn_stream_t *
@@ -864,23 +860,11 @@ void
 svn_stream_set_seek(svn_stream_t *stream,
                     svn_io_seek_fn_t seek_fn);
 
-/** Set @a stream's line-filtering callback function to @a line_filter_cb
- *
- * @since New in 1.7.
- */
+/** Set @a stream's readline function to @a readline_fn */
 void
-svn_stream_set_line_filter_callback(svn_stream_t *stream,
-                                    svn_io_line_filter_cb_t line_filter_cb);
+svn_stream_set_readline(svn_stream_t *stream,
+                        svn_io_readline_fn_t readline_fn);
 
-/** Set @a streams's line-transforming callback function to
- * @a line_transformer_cb.
- *
- * @since New in 1.7.
- */
-void
-svn_stream_set_line_transformer_callback(
-  svn_stream_t *stream,
-  svn_io_line_transformer_cb_t line_transformer_cb);
 
 /** Create a stream that is empty for reading and infinite for writing. */
 svn_stream_t *
@@ -1203,19 +1187,6 @@ svn_stream_printf_from_utf8(svn_stream_t *stream,
  *
  * If @a stream runs out of bytes before encountering a line-terminator,
  * then set @a *eof to @c TRUE, otherwise set @a *eof to FALSE.
- *
- * If a line-filter callback function was set on the stream using
- * svn_stream_set_line_filter_callback(), lines will only be returned
- * if they pass the filtering decision of the callback function.
- * If end-of-file is encountered while reading the line and the line
- * is filtered, @a *stringbuf will be empty.
- *
- * If a line-transformer callback function was set on the stream using
- * svn_stream_set_line_transformer_callback(), lines will be returned
- * transformed, in a way determined by the callback.
- *
- * Note that the line-transformer callback gets called after the line-filter
- * callback, not before.
  */
 svn_error_t *
 svn_stream_readline(svn_stream_t *stream,
@@ -1225,11 +1196,14 @@ svn_stream_readline(svn_stream_t *stream,
                     apr_pool_t *pool);
 
 /**
- * Similar to svn_stream_readline(). The line-terminator is detected
- * automatically.  If @a eol is not NULL, the detected line-terminator
- * is returned in @a *eol.  If EOF is reached and the stream does not
- * end with a newline character, @a *eol will be NULL.
+ * Similar to svn_stream_readline(). If @a *eol is not NULL, it is used
+ * as the expected line terminator. If @a eol is NULL, the line-terminator
+ * is detected automatically. If @a *eol is NULL, the line-terminator is
+ * detected automatically and is returned in @a *eol.
  *
+ * The @a *stringbuf will be allocated in @a result_pool.
+ * @a scratch_pool is used for temporary allocations.
+ * 
  * @since New in 1.7.
  */
 svn_error_t *
@@ -1237,7 +1211,8 @@ svn_stream_readline_detect_eol(svn_stream_t *strea
                                svn_stringbuf_t **stringbuf,
                                const char **eol,
                                svn_boolean_t *eof,
-                               apr_pool_t *pool);
+                               apr_pool_t *result_pool,
+                               apr_pool_t *scratch_pool);
 
 /**
  * Read the contents of the readable stream @a from and write them to the
Index: subversion/libsvn_diff/parse-diff.c
===================================================================
--- subversion/libsvn_diff/parse-diff.c	(revision 961349)
+++ subversion/libsvn_diff/parse-diff.c	(working copy)
@@ -33,6 +33,8 @@
 #include "svn_dirent_uri.h"
 #include "svn_diff.h"
 
+#include "private/svn_eol_private.h"
+
 /* Helper macro for readability */
 #define starts_with(str, start)  \
   (strncmp((str), (start), strlen(start)) == 0)
@@ -184,89 +186,398 @@ parse_hunk_header(const char *header, svn_hunk_t *
   return TRUE;
 }
 
-/* A stream line-filter which allows only original text from a hunk,
- * and filters special lines (which start with a backslash). */
+/* Users of the diff parsing API are provided with various streams
+ * to read lines from a hunk. These streams return:
+ *
+ *  - the original hunk text (all lines starting with '-' or ' ')
+ *  - the modified hunk text (all lines starting with '+' or ' ')
+ *  - the plain unidiff text (possibly reversed)
+ *
+ * To achieve this, we wrap the patch file stream with custom streams,
+ * which override the wrapped stream's readline method. */
+
+/* Baton for a stream that reads hunk texts. */
+struct hunk_text_stream_baton {
+  /* The leading unidiff symbol of lines which should be filtered.
+   * Can be '+' or '-', depending on whether we're providing the original
+   * or modified version of the hunk. */
+  char verboten;
+
+  svn_stream_t *wrapped_stream;
+};
+
+/* An implementation of svn_read_fn_t. */
 static svn_error_t *
-original_line_filter(svn_boolean_t *filtered, const char *line, void *baton,
-                     apr_pool_t *scratch_pool)
+read_handler_hunk_text(void *baton, char *buffer, apr_size_t *len)
 {
-  *filtered = (line[0] == '+' || line[0] == '\\');
-  return SVN_NO_ERROR;
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_read(b->wrapped_stream, buffer, len);
 }
 
-/* A stream line-filter which allows only modified text from a hunk,
- * and filters special lines (which start with a backslash). */
+/* An implementation of svn_write_fn_t. */
 static svn_error_t *
-modified_line_filter(svn_boolean_t *filtered, const char *line, void *baton,
-                     apr_pool_t *scratch_pool)
+write_handler_hunk_text(void *baton, const char *buffer, apr_size_t *len)
 {
-  *filtered = (line[0] == '-' || line[0] == '\\');
-  return SVN_NO_ERROR;
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_write(b->wrapped_stream, buffer, len);
 }
 
-/** line-transformer callback to shave leading diff symbols. */
+/* An implementation of svn_close_fn_t. */
 static svn_error_t *
-remove_leading_char_transformer(svn_stringbuf_t **buf,
-                                const char *line,
-                                void *baton,
-                                apr_pool_t *result_pool,
-                                apr_pool_t *scratch_pool)
+close_handler_hunk_text(void *baton)
 {
-  if (line[0] == '+' || line[0] == '-' || line[0] == ' ')
-    *buf = svn_stringbuf_create(line + 1, result_pool);
-  else
-    *buf = svn_stringbuf_create(line, result_pool);
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_close(b->wrapped_stream);
+}
 
+/* An implementation of svn_io_reset_fn_t. */
+static svn_error_t *
+reset_handler_hunk_text(void *baton)
+{
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_reset(b->wrapped_stream);
+}
+
+/* An implementation of svn_io_mark_fn_t. */
+static svn_error_t *
+mark_handler_hunk_text(void *baton, svn_stream_mark_t **mark, apr_pool_t *pool)
+{
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_mark(b->wrapped_stream, mark, pool);
+}
+
+/* An implementation of svn_io_seek_fn_t. */
+static svn_error_t *
+seek_handler_hunk_text(void *baton, svn_stream_mark_t *mark)
+{
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_seek(b->wrapped_stream, mark);
+}
+
+/* Invoke the READ_FN function of a stream to scan for an end-of-line
+ * indicator, and return it in *EOL.
+ * Set *EOL to NULL if the stream runs out before an end-of-line indicator
+ * is found. */
+static svn_error_t *
+scan_eol(void *baton,
+         svn_read_fn_t read_fn,
+         svn_io_mark_fn_t mark_fn,
+         svn_io_seek_fn_t seek_fn,
+         const char **eol,
+         apr_pool_t *pool)
+{
+  const char *eol_str;
+  svn_stream_mark_t *mark;
+
+  SVN_ERR(mark_fn(baton, &mark, pool));
+
+  eol_str = NULL;
+  while (! eol_str)
+    {
+      char buf[512];
+      apr_size_t len;
+
+      len = sizeof(buf);
+      SVN_ERR(read_fn(baton, buf, &len));
+      if (len == 0)
+          break; /* EOF */
+      eol_str = svn_eol__detect_eol(buf, buf + len);
+    }
+
+  SVN_ERR(seek_fn(baton, mark));
+
+  *eol = eol_str;
+
   return SVN_NO_ERROR;
 }
 
-/** line-transformer callback to reverse a diff text. */
+/* An implementation of svn_io_readline_fn_t. */
 static svn_error_t *
-reverse_diff_transformer(svn_stringbuf_t **buf,
-                         const char *line,
-                         void *baton,
-                         apr_pool_t *result_pool,
-                         apr_pool_t *scratch_pool)
+readline_handler_hunk_text(void *baton,
+                           svn_read_fn_t read_fn,
+                           svn_io_mark_fn_t mark_fn,
+                           svn_io_seek_fn_t seek_fn,
+                           svn_stringbuf_t **stringbuf,
+                           const char **eol,
+                           svn_boolean_t *eof,
+                           apr_pool_t *result_pool,
+                           apr_pool_t *scratch_pool)
 {
-  svn_hunk_t hunk;
+  svn_stringbuf_t *str;
+  apr_pool_t *iterpool;
+  svn_boolean_t filtered;
+  const char *eol_str;
+  struct hunk_text_stream_baton *b = baton;
 
-  /* ### Pass the already parsed hunk via the baton?
-   * ### Maybe we should really make svn_stream_readline() a proper stream
-   * ### method and override it instead of adding special callbacks? */
-  if (parse_hunk_header(line, &hunk, "@@", FALSE, scratch_pool))
+  *eof = FALSE;
+
+  iterpool = svn_pool_create(scratch_pool);
+  do
     {
-      *buf = svn_stringbuf_createf(result_pool,
-                                   "@@ -%lu,%lu +%lu,%lu @@",
-                                   hunk.modified_start,
-                                   hunk.modified_length,
-                                   hunk.original_start,
-                                   hunk.original_length);
+      apr_size_t numbytes;
+      const char *match;
+      char c;
+
+      svn_pool_clear(iterpool);
+
+      /* Since we're reading one character at a time, let's at least
+         optimize for the 90% case.  90% of the time, we can avoid the
+         stringbuf ever having to realloc() itself if we start it out at
+         80 chars.  */
+      str = svn_stringbuf_create_ensure(80, iterpool);
+
+      if (eol == NULL || *eol == NULL)
+        {
+          SVN_ERR(scan_eol(baton, read_fn, mark_fn, seek_fn,
+                           &eol_str, iterpool));
+          if (! eol_str)
+            {
+              /* No newline until EOF, EOL_STR can be anything. */
+              eol_str = APR_EOL_STR;
+            }
+          if (eol)
+            *eol = eol_str;
+        }
+      else
+        eol_str = *eol;
+
+      /* Read into STR up to and including the next EOL sequence. */
+      match = eol_str;
+      numbytes = 1;
+      while (*match)
+        {
+          SVN_ERR(read_fn(baton, &c, &numbytes));
+          if (numbytes != 1)
+            {
+              /* a 'short' read means the stream has run out. */
+              *eof = TRUE;
+              /* We know we don't have a whole EOL sequence, but ensure we
+               * don't chop off any partial EOL sequence that we may have. */
+              match = eol_str;
+              /* Process this short (or empty) line just like any other
+               * except with *EOF set. */
+              break;
+            }
+
+          if (c == *match)
+            match++;
+          else
+            match = eol_str;
+
+          svn_stringbuf_appendbytes(str, &c, 1);
+        }
+
+      svn_stringbuf_chop(str, match - eol_str);
+      filtered = (str->data[0] == b->verboten || str->data[0] == '\\');
     }
-  else if (parse_hunk_header(line, &hunk, "##", FALSE, scratch_pool))
+  while (filtered && ! *eof);
+  /* Not destroying the iterpool just yet since we still need STR
+   * which is allocated in it. */
+
+  if (filtered)
     {
-      *buf = svn_stringbuf_createf(result_pool,
-                                   "## -%lu,%lu +%lu,%lu ##",
-                                   hunk.modified_start,
-                                   hunk.modified_length,
-                                   hunk.original_start,
-                                   hunk.original_length);
+      /* EOF, return an empty string. */
+      *stringbuf = svn_stringbuf_create_ensure(0, result_pool);
     }
-  else if (line[0] == '+')
+  else if (str->data[0] == '+' || str->data[0] == '-' || str->data[0] == ' ')
     {
-      *buf = svn_stringbuf_create(line, result_pool);
-      (*buf)->data[0] = '-';
+      /* Shave off leading unidiff symbols. */
+      *stringbuf = svn_stringbuf_create(str->data + 1, result_pool);
     }
-  else if (line[0] == '-')
+  else
     {
-      *buf = svn_stringbuf_create(line, result_pool);
-      (*buf)->data[0] = '+';
+      /* Return the line as-is. */
+      *stringbuf = svn_stringbuf_dup(str, result_pool);
     }
+
+  /* Done. RIP iterpool. */
+  svn_pool_destroy(iterpool);
+
+  return SVN_NO_ERROR;
+}
+
+/* Return a stream for reading hunk text from WRAPPED_STREAM.
+ * VERBOTEN is the leading character of lines which should be
+ * filtered by this stream's readline method ('+' or '-').
+ * Allocate the new stream in RESULT_POOL. */
+static svn_stream_t *
+stream_hunk_text(svn_stream_t *wrapped_stream,
+                 char verboten,
+                 apr_pool_t *result_pool)
+{
+  svn_stream_t *stream;
+  struct hunk_text_stream_baton *baton;
+
+  baton = apr_palloc(result_pool, sizeof(*baton));
+  baton->wrapped_stream = wrapped_stream;
+  baton->verboten = verboten;
+
+  stream = svn_stream_create(baton, result_pool);
+
+  svn_stream_set_read(stream, read_handler_hunk_text);
+  svn_stream_set_write(stream, write_handler_hunk_text);
+  svn_stream_set_close(stream, close_handler_hunk_text);
+  svn_stream_set_reset(stream, reset_handler_hunk_text);
+  svn_stream_set_mark(stream, mark_handler_hunk_text);
+  svn_stream_set_seek(stream, seek_handler_hunk_text);
+  svn_stream_set_readline(stream, readline_handler_hunk_text);
+
+  return stream;
+}
+
+/* Return a stream to read the original text of a hunk from
+ * WRAPPED_STREAM, and allocated in RESULT_POOL.*/
+static svn_stream_t *
+stream_hunk_text_original(svn_stream_t *wrapped_stream,
+                          apr_pool_t *result_pool)
+{
+  return stream_hunk_text(wrapped_stream, '+', result_pool);
+}
+
+/* Return a stream to read the modified text of a hunk from
+ * WRAPPED_STREAM, and allocated in RESULT_POOL.*/
+static svn_stream_t *
+stream_hunk_text_modified(svn_stream_t *wrapped_stream,
+                          apr_pool_t *result_pool)
+{
+  return stream_hunk_text(wrapped_stream, '-', result_pool);
+}
+
+
+/* Baton for a stream that reads unidiff text reversed. */
+struct reverse_diff_text_stream_baton {
+  svn_stream_t *wrapped_stream;
+}; 
+
+/* An implementation of svn_read_fn_t. */
+static svn_error_t *
+read_handler_reverse_diff_text(void *baton, char *buffer, apr_size_t *len)
+{
+  struct reverse_diff_text_stream_baton *b = baton;
+  return svn_stream_read(b->wrapped_stream, buffer, len);
+}
+
+/* An implementation of svn_write_fn_t. */
+static svn_error_t *
+write_handler_reverse_diff_text(void *baton, const char *buffer,
+                                apr_size_t *len)
+{
+  struct reverse_diff_text_stream_baton *b = baton;
+  return svn_stream_write(b->wrapped_stream, buffer, len);
+}
+
+/* An implementation of svn_close_fn_t. */
+static svn_error_t *
+close_handler_reverse_diff_text(void *baton)
+{
+  struct reverse_diff_text_stream_baton *b = baton;
+  return svn_stream_close(b->wrapped_stream);
+}
+
+/* An implementation of svn_io_reset_fn_t. */
+static svn_error_t *
+reset_handler_reverse_diff_text(void *baton)
+{
+  struct reverse_diff_text_stream_baton *b = baton;
+  return svn_stream_reset(b->wrapped_stream);
+}
+
+/* An implementation of svn_io_mark_fn_t. */
+static svn_error_t *
+mark_handler_reverse_diff_text(void *baton, svn_stream_mark_t **mark,
+                               apr_pool_t *pool)
+{
+  struct reverse_diff_text_stream_baton *b = baton;
+  return svn_stream_mark(b->wrapped_stream, mark, pool);
+}
+
+/* An implementation of svn_io_seek_fn_t. */
+static svn_error_t *
+seek_handler_reverse_diff_text(void *baton, svn_stream_mark_t *mark)
+{
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_seek(b->wrapped_stream, mark);
+}
+
+/* An implementation of svn_io_readline_fn_t. */
+static svn_error_t *
+readline_handler_reverse_diff_text(void *baton,
+                                   svn_read_fn_t read_fn,
+                                   svn_io_mark_fn_t mark_fn,
+                                   svn_io_seek_fn_t seek_fn,
+                                   svn_stringbuf_t **stringbuf,
+                                   const char **eol,
+                                   svn_boolean_t *eof,
+                                   apr_pool_t *result_pool,
+                                   apr_pool_t *scratch_pool)
+{
+  svn_hunk_t hunk;
+  svn_stringbuf_t *line;
+  struct reverse_diff_text_stream_baton *b = baton;
+
+  /* Read the line and perform necessary transformations to
+   * produce a reversed diff. */
+  SVN_ERR(svn_stream_readline_detect_eol(b->wrapped_stream,
+                                         &line, eol, eof,
+                                         result_pool, scratch_pool));
+  if (parse_hunk_header(line->data, &hunk, "@@", FALSE, scratch_pool))
+    {
+      /* Line is a hunk header, reverse it. */
+      *stringbuf = svn_stringbuf_createf(result_pool,
+                                         "@@ -%lu,%lu +%lu,%lu @@",
+                                         hunk.modified_start,
+                                         hunk.modified_length,
+                                         hunk.original_start,
+                                         hunk.original_length);
+    }
+  else if (parse_hunk_header(line->data, &hunk, "##", FALSE, scratch_pool))
+    {
+      /* Line is a hunk header, reverse it. */
+      *stringbuf = svn_stringbuf_createf(result_pool,
+                                         "## -%lu,%lu +%lu,%lu ##",
+                                         hunk.modified_start,
+                                         hunk.modified_length,
+                                         hunk.original_start,
+                                         hunk.original_length);
+    }
   else
-    *buf = svn_stringbuf_create(line, result_pool);
+    {
+      if (line->data[0] == '+')
+        line->data[0] = '-';
+      else if (line->data[0] == '-')
+        line->data[0] = '+';
 
+      *stringbuf = line;
+    }
+
   return SVN_NO_ERROR;
 }
 
+/* Return a stream for reading diff text from WRAPPED_STREAM.
+ * The unidiff will appear reversed when read via the stream's readline method.
+ * Allocate the new stream in RESULT_POOL. */
+static svn_stream_t *
+stream_reverse_diff_text(svn_stream_t *wrapped_stream,
+                         apr_pool_t *result_pool)
+{
+  svn_stream_t *stream;
+  struct reverse_diff_text_stream_baton *baton;
+
+  baton = apr_palloc(result_pool, sizeof(*baton));
+  baton->wrapped_stream = wrapped_stream;
+
+  stream = svn_stream_create(baton, result_pool);
+  svn_stream_set_read(stream, read_handler_reverse_diff_text);
+  svn_stream_set_write(stream, write_handler_reverse_diff_text);
+  svn_stream_set_close(stream, close_handler_reverse_diff_text);
+  svn_stream_set_reset(stream, reset_handler_reverse_diff_text);
+  svn_stream_set_mark(stream, mark_handler_reverse_diff_text);
+  svn_stream_set_seek(stream, seek_handler_reverse_diff_text);
+  svn_stream_set_readline(stream, readline_handler_reverse_diff_text);
+
+  return stream;
+}
+
 /* Parse PROP_NAME from HEADER as the part after the INDICATOR line. */
 static svn_error_t *
 parse_prop_name(const char **prop_name, const char *header, 
@@ -349,7 +660,7 @@ parse_next_hunk(svn_hunk_t **hunk,
       /* Remember the current line's offset, and read the line. */
       last_line = pos;
       SVN_ERR(svn_stream_readline_detect_eol(stream, &line, NULL, &eof,
-                                             iterpool));
+                                             iterpool, iterpool));
 
       if (! eof)
         {
@@ -502,38 +813,41 @@ parse_next_hunk(svn_hunk_t **hunk,
       apr_file_t *f;
       apr_int32_t flags = APR_READ | APR_BUFFERED;
 
+      /* For each of the streams created below, we re-open the patch file.
+       * Each stream needs its own file descriptor in order to have
+       * independent seek behaviour. */
+
       /* Create a stream which returns the hunk text itself. */
       SVN_ERR(svn_io_file_open(&f, patch->path, flags, APR_OS_DEFAULT,
                                result_pool));
       diff_text = svn_stream_from_aprfile_range_readonly(f, FALSE,
                                                          start, end,
                                                          result_pool);
+      if (reverse)
+        diff_text = stream_reverse_diff_text(diff_text, result_pool);
 
       /* Create a stream which returns the original hunk text. */
       SVN_ERR(svn_io_file_open(&f, patch->path, flags, APR_OS_DEFAULT,
                                result_pool));
-      original_text = svn_stream_from_aprfile_range_readonly(f, FALSE,
-                                                             start, end,
-                                                             result_pool);
-      svn_stream_set_line_filter_callback(original_text, original_line_filter);
-      svn_stream_set_line_transformer_callback(original_text,
-                                               remove_leading_char_transformer);
+      original_text = stream_hunk_text_original(
+                        svn_stream_from_aprfile_range_readonly(f, FALSE,
+                                                               start, end,
+                                                               result_pool),
+                        result_pool);
 
       /* Create a stream which returns the modified hunk text. */
       SVN_ERR(svn_io_file_open(&f, patch->path, flags, APR_OS_DEFAULT,
                                result_pool));
-      modified_text = svn_stream_from_aprfile_range_readonly(f, FALSE,
-                                                             start, end,
-                                                             result_pool);
-      svn_stream_set_line_filter_callback(modified_text, modified_line_filter);
-      svn_stream_set_line_transformer_callback(modified_text,
-                                               remove_leading_char_transformer);
+      modified_text = stream_hunk_text_modified(
+                        svn_stream_from_aprfile_range_readonly(f, FALSE,
+                                                               start, end,
+                                                               result_pool),
+                        result_pool);
+
       /* Set the hunk's texts. */
       (*hunk)->diff_text = diff_text;
       if (reverse)
         {
-          svn_stream_set_line_transformer_callback(diff_text,
-                                                   reverse_diff_transformer);
           (*hunk)->original_text = modified_text;
           (*hunk)->modified_text = original_text;
         }
@@ -918,7 +1232,7 @@ svn_diff_parse_next_patch(svn_patch_t **patch,
       /* Remember the current line's offset, and read the line. */
       last_line = pos;
       SVN_ERR(svn_stream_readline_detect_eol(stream, &line, NULL, &eof,
-                                             iterpool));
+                                             iterpool, iterpool));
 
       if (! eof)
         {
Index: subversion/libsvn_subr/stream.c
===================================================================
--- subversion/libsvn_subr/stream.c	(revision 961349)
+++ subversion/libsvn_subr/stream.c	(working copy)
@@ -53,8 +53,7 @@ struct svn_stream_t {
   svn_io_reset_fn_t reset_fn;
   svn_io_mark_fn_t mark_fn;
   svn_io_seek_fn_t seek_fn;
-  svn_io_line_filter_cb_t line_filter_cb;
-  svn_io_line_transformer_cb_t line_transformer_cb;
+  svn_io_readline_fn_t readline_fn;
 };
 
 
@@ -73,8 +72,7 @@ svn_stream_create(void *baton, apr_pool_t *pool)
   stream->reset_fn = NULL;
   stream->mark_fn = NULL;
   stream->seek_fn = NULL;
-  stream->line_filter_cb = NULL;
-  stream->line_transformer_cb = NULL;
+  stream->readline_fn = NULL;
   return stream;
 }
 
@@ -124,20 +122,11 @@ svn_stream_set_seek(svn_stream_t *stream, svn_io_s
 }
 
 void
-svn_stream_set_line_filter_callback(svn_stream_t *stream,
-                                    svn_io_line_filter_cb_t line_filter_cb)
+svn_stream_set_readline(svn_stream_t *stream, svn_io_readline_fn_t readline_fn)
 {
-  stream->line_filter_cb = line_filter_cb;
+  stream->readline_fn = readline_fn;
 }
 
-void
-svn_stream_set_line_transformer_callback(
-  svn_stream_t *stream,
-  svn_io_line_transformer_cb_t line_transformer_cb)
-{
-  stream->line_transformer_cb = line_transformer_cb;
-}
-
 svn_error_t *
 svn_stream_read(svn_stream_t *stream, char *buffer, apr_size_t *len)
 {
@@ -233,55 +222,23 @@ svn_stream_printf_from_utf8(svn_stream_t *stream,
   return svn_stream_write(stream, translated, &len);
 }
 
-/* If a line filter callback was set on STREAM, invoke it on LINE,
- * and indicate in *FILTERED whether the line should be filtered.
- * If no line filter callback was set on STREAM, just set *FILTERED to FALSE.
- */
-static svn_error_t *
-line_filter(svn_stream_t *stream, svn_boolean_t *filtered, const char *line,
-            apr_pool_t *pool)
-{
-  apr_pool_t *scratch_pool;
-
-  if (! stream->line_filter_cb)
-    {
-      *filtered = FALSE;
-      return SVN_NO_ERROR;
-    }
-
-  scratch_pool = svn_pool_create(pool);
-  SVN_ERR(stream->line_filter_cb(filtered, line, stream->baton, scratch_pool));
-  svn_pool_destroy(scratch_pool);
-  return SVN_NO_ERROR;
-}
-
-/* Run the line transformer callback of STREAM with LINE as input,
- * and expect the transformation result to be returned in BUF,
- * allocated in POOL. */
-static svn_error_t *
-line_transformer(svn_stream_t *stream, svn_stringbuf_t **buf,
-                 const char *line, apr_pool_t *pool, apr_pool_t *scratch_pool)
-{
-  *buf = NULL;  /* so we can assert that the callback has set it non-null */
-  SVN_ERR(stream->line_transformer_cb(buf, line, stream->baton,
-                                      pool, scratch_pool));
-
-  /* Die if the line transformer didn't provide any output. */
-  SVN_ERR_ASSERT(*buf);
-
-  return SVN_NO_ERROR;
-}
-
-/* Scan STREAM for an end-of-line indicatior, and return it in *EOL.
+/* Invoke the READ_FN function of a stream to scan  for an end-of-line
+ * indicatior, and return it in *EOL.
+ * Use MARK_FN and SEEK_FN to seek in the stream.
  * Set *EOL to NULL if the stream runs out before an end-of-line indicator
  * is found. */
 static svn_error_t *
-scan_eol(const char **eol, svn_stream_t *stream, apr_pool_t *pool)
+scan_eol(void *baton,
+         svn_read_fn_t read_fn,
+         svn_io_mark_fn_t mark_fn,
+         svn_io_seek_fn_t seek_fn,
+         const char **eol,
+         apr_pool_t *pool)
 {
   const char *eol_str;
   svn_stream_mark_t *mark;
 
-  SVN_ERR(svn_stream_mark(stream, &mark, pool));
+  SVN_ERR(mark_fn(baton, &mark, pool));
 
   eol_str = NULL;
   while (! eol_str)
@@ -290,113 +247,82 @@ static svn_error_t *
       apr_size_t len;
 
       len = sizeof(buf);
-      SVN_ERR(svn_stream_read(stream, buf, &len));
+      SVN_ERR(read_fn(baton, buf, &len));
       if (len == 0)
           break; /* EOF */
       eol_str = svn_eol__detect_eol(buf, buf + len);
     }
 
-  SVN_ERR(svn_stream_seek(stream, mark));
+  SVN_ERR(seek_fn(baton, mark));
 
   *eol = eol_str;
 
   return SVN_NO_ERROR;
 }
 
-/* Guts of svn_stream_readline() and svn_stream_readline_detect_eol().
- * Returns the line read from STREAM in *STRINGBUF, and indicates
- * end-of-file in *EOF.  If DETECT_EOL is TRUE, the end-of-line indicator
- * is detected automatically and returned in *EOL.
- * If DETECT_EOL is FALSE, *EOL must point to the desired end-of-line
- * indicator.  STRINGBUF is allocated in POOL. */
+/* An implementation of svn_io_readline_fn_t */
 static svn_error_t *
-stream_readline(svn_stringbuf_t **stringbuf,
-                svn_boolean_t *eof,
+stream_readline(void *baton,
+                svn_read_fn_t read_fn,
+                svn_io_mark_fn_t mark_fn,
+                svn_io_seek_fn_t seek_fn,
+                svn_stringbuf_t **stringbuf,
                 const char **eol,
-                svn_stream_t *stream,
-                svn_boolean_t detect_eol,
-                apr_pool_t *pool)
+                svn_boolean_t *eof,
+                apr_pool_t *result_pool,
+                apr_pool_t *scratch_pool)
 {
-  svn_stringbuf_t *str;
-  apr_pool_t *iterpool;
-  svn_boolean_t filtered;
+  apr_size_t numbytes;
+  const char *match;
+  char c;
   const char *eol_str;
+  /* Since we're reading one character at a time, let's at least
+     optimize for the 90% case.  90% of the time, we can avoid the
+     stringbuf ever having to realloc() itself if we start it out at
+     80 chars.  */
+  svn_stringbuf_t *str = svn_stringbuf_create_ensure(80, scratch_pool);
 
-  *eof = FALSE;
-
-  iterpool = svn_pool_create(pool);
-  do
+  if (eol == NULL || *eol == NULL)
     {
-      apr_size_t numbytes;
-      const char *match;
-      char c;
-
-      svn_pool_clear(iterpool);
-
-      /* Since we're reading one character at a time, let's at least
-         optimize for the 90% case.  90% of the time, we can avoid the
-         stringbuf ever having to realloc() itself if we start it out at
-         80 chars.  */
-      str = svn_stringbuf_create_ensure(80, iterpool);
-
-      if (detect_eol)
+      SVN_ERR(scan_eol(baton, read_fn, mark_fn, seek_fn,
+                       &eol_str, scratch_pool));
+      if (! eol_str)
         {
-          SVN_ERR(scan_eol(&eol_str, stream, iterpool));
-          if (eol)
-            *eol = eol_str;
-          if (! eol_str)
-            {
-              /* No newline until EOF, EOL_STR can be anything. */
-              eol_str = APR_EOL_STR;
-            }
+          /* No newline until EOF, EOL_STR can be anything. */
+          eol_str = APR_EOL_STR;
         }
-      else
-        eol_str = *eol;
 
-      /* Read into STR up to and including the next EOL sequence. */
-      match = eol_str;
+      if (eol)
+        *eol = eol_str;
+    }
+  else
+    eol_str = *eol;
+
+  /* Read into STR up to and including the next EOL sequence. */
+  match = eol_str;
+  while (*match)
+    {
       numbytes = 1;
-      while (*match)
+      SVN_ERR(read_fn(baton, &c, &numbytes));
+      if (numbytes != 1)
         {
-          SVN_ERR(svn_stream_read(stream, &c, &numbytes));
-          if (numbytes != 1)
-            {
-              /* a 'short' read means the stream has run out. */
-              *eof = TRUE;
-              /* We know we don't have a whole EOL sequence, but ensure we
-               * don't chop off any partial EOL sequence that we may have. */
-              match = eol_str;
-              /* Process this short (or empty) line just like any other
-               * except with *EOF set. */
-              break;
-            }
-
-          if (c == *match)
-            match++;
-          else
-            match = eol_str;
-
-          svn_stringbuf_appendbytes(str, &c, 1);
+          /* a 'short' read means the stream has run out. */
+          *eof = TRUE;
+          *stringbuf = svn_stringbuf_dup(str, result_pool);
+          return SVN_NO_ERROR;
         }
 
-      svn_stringbuf_chop(str, match - eol_str);
+      if (c == *match)
+        match++;
+      else
+        match = eol_str;
 
-      SVN_ERR(line_filter(stream, &filtered, str->data, iterpool));
+      svn_stringbuf_appendbytes(str, &c, 1);
     }
-  while (filtered && ! *eof);
-  /* Not destroying the iterpool just yet since we still need STR
-   * which is allocated in it. */
 
-  if (filtered)
-    *stringbuf = svn_stringbuf_create_ensure(0, pool);
-  else if (stream->line_transformer_cb)
-    SVN_ERR(line_transformer(stream, stringbuf, str->data, pool, iterpool));
-  else
-    *stringbuf = svn_stringbuf_dup(str, pool);
-
-  /* Done. RIP iterpool. */
-  svn_pool_destroy(iterpool);
-
+  *eof = FALSE;
+  svn_stringbuf_chop(str, match - eol_str);
+  *stringbuf = svn_stringbuf_dup(str, result_pool);
   return SVN_NO_ERROR;
 }
 
@@ -407,8 +333,25 @@ svn_stream_readline(svn_stream_t *stream,
                     svn_boolean_t *eof,
                     apr_pool_t *pool)
 {
-  return svn_error_return(stream_readline(stringbuf, eof, &eol, stream,
-                                          FALSE, pool));
+  svn_io_readline_fn_t readline_fn;
+
+  /* Provide a default readline implementation if the stream
+   * hasn't overriden it. This is needed for backwards compat
+   * to 1.6.x and earlier. */
+  if (stream->readline_fn)
+    readline_fn = stream->readline_fn;
+  else
+    readline_fn = stream_readline;
+
+  /* Caller must provide an EOL character. */
+  SVN_ERR_ASSERT(eol && *eol);
+
+  return svn_error_return(readline_fn(stream->baton,
+                                      stream->read_fn,
+                                      stream->mark_fn,
+                                      stream->seek_fn,
+                                      stringbuf, &eol, eof,
+                                      pool, pool));
 }
 
 svn_error_t *
@@ -416,13 +359,35 @@ svn_stream_readline_detect_eol(svn_stream_t *strea
                                svn_stringbuf_t **stringbuf,
                                const char **eol,
                                svn_boolean_t *eof,
-                               apr_pool_t *pool)
+                               apr_pool_t *result_pool,
+                               apr_pool_t *scratch_pool)
 {
-  return svn_error_return(stream_readline(stringbuf, eof, eol, stream,
-                                          TRUE, pool));
-}
+  svn_io_readline_fn_t readline_fn;
 
+  /* Provide a default readline implementation if the stream
+   * hasn't overriden it. This is not needed for backwards compat
+   * to 1.6.x and earlier (this function is new in 1.7), but it
+   * is nice anyway because it saves us from adding dummy readline
+   * methods to custom streams sprinkled throughout the code. */
+  if (stream->readline_fn)
+    readline_fn = stream->readline_fn;
+  else
+    readline_fn = stream_readline;
 
+  /* EOL-detection requires mark/seek support. */
+  if (stream->mark_fn == NULL)
+    return svn_error_create(SVN_ERR_STREAM_SEEK_NOT_SUPPORTED, NULL, NULL);
+  if (stream->seek_fn == NULL)
+    return svn_error_create(SVN_ERR_STREAM_SEEK_NOT_SUPPORTED, NULL, NULL);
+
+  return svn_error_return(readline_fn(stream->baton,
+                                      stream->read_fn,
+                                      stream->mark_fn,
+                                      stream->seek_fn,
+                                      stringbuf, eol, eof,
+                                      result_pool, scratch_pool));
+}
+
 svn_error_t *svn_stream_copy3(svn_stream_t *from, svn_stream_t *to,
                               svn_cancel_func_t cancel_func,
                               void *cancel_baton,
@@ -536,7 +501,27 @@ seek_handler_empty(void *baton, svn_stream_mark_t
   return SVN_NO_ERROR;
 }
 
+static svn_error_t *
+readline_handler_empty(void *baton,
+                       svn_read_fn_t read_fn,
+                       svn_io_mark_fn_t mark_fn,
+                       svn_io_seek_fn_t seek_fn,
+                       svn_stringbuf_t **stringbuf,
+                       const char **eol,
+                       svn_boolean_t *eof,
+                       apr_pool_t *result_pool,
+                       apr_pool_t *scratch_pool)
+{
+  *stringbuf = svn_stringbuf_create_ensure(0, result_pool);
 
+  if (eol && *eol == NULL)
+    *eol = APR_EOL_STR;
+
+  *eof = TRUE;
+
+  return SVN_NO_ERROR;
+}
+
 svn_stream_t *
 svn_stream_empty(apr_pool_t *pool)
 {
@@ -548,6 +533,8 @@ svn_stream_empty(apr_pool_t *pool)
   svn_stream_set_reset(stream, reset_handler_empty);
   svn_stream_set_mark(stream, mark_handler_empty);
   svn_stream_set_seek(stream, seek_handler_empty);
+  svn_stream_set_readline(stream, readline_handler_empty);
+
   return stream;
 }
 
@@ -652,6 +639,7 @@ svn_stream_disown(svn_stream_t *stream, apr_pool_t
   svn_stream_set_reset(s, reset_handler_disown);
   svn_stream_set_mark(s, mark_handler_disown);
   svn_stream_set_seek(s, seek_handler_disown);
+  svn_stream_set_readline(s, stream_readline);
 
   return s;
 }
@@ -820,6 +808,7 @@ svn_stream_from_aprfile2(apr_file_t *file,
   svn_stream_set_reset(stream, reset_handler_apr);
   svn_stream_set_mark(stream, mark_handler_apr);
   svn_stream_set_seek(stream, seek_handler_apr);
+  svn_stream_set_readline(stream, stream_readline);
 
   if (! disown)
     svn_stream_set_close(stream, close_handler_apr);
@@ -898,6 +887,7 @@ svn_stream_from_aprfile_range_readonly(apr_file_t
   svn_stream_set_reset(stream, reset_handler_apr);
   svn_stream_set_mark(stream, mark_handler_apr);
   svn_stream_set_seek(stream, seek_handler_apr);
+  svn_stream_set_readline(stream, stream_readline);
 
   if (! disown)
     svn_stream_set_close(stream, close_handler_apr);
@@ -1187,6 +1177,7 @@ svn_stream_compressed(svn_stream_t *stream, apr_po
   svn_stream_set_read(zstream, read_handler_gz);
   svn_stream_set_write(zstream, write_handler_gz);
   svn_stream_set_close(zstream, close_handler_gz);
+  svn_stream_set_readline(zstream, stream_readline);
 
   return zstream;
 }
@@ -1302,6 +1293,8 @@ svn_stream_checksummed2(svn_stream_t *stream,
   svn_stream_set_read(s, read_handler_checksum);
   svn_stream_set_write(s, write_handler_checksum);
   svn_stream_set_close(s, close_handler_checksum);
+  svn_stream_set_readline(s, stream_readline);
+
   return s;
 }
 
@@ -1384,6 +1377,8 @@ svn_stream_checksummed(svn_stream_t *stream,
   svn_stream_set_read(s, read_handler_md5);
   svn_stream_set_write(s, write_handler_md5);
   svn_stream_set_close(s, close_handler_md5);
+  svn_stream_set_readline(s, stream_readline);
+
   return s;
 }
 
@@ -1475,6 +1470,8 @@ svn_stream_from_stringbuf(svn_stringbuf_t *str,
   svn_stream_set_reset(stream, reset_handler_stringbuf);
   svn_stream_set_mark(stream, mark_handler_stringbuf);
   svn_stream_set_seek(stream, seek_handler_stringbuf);
+  svn_stream_set_readline(stream, stream_readline);
+
   return stream;
 }
 
@@ -1511,6 +1508,8 @@ svn_stream_from_string(const svn_string_t *str,
   baton->amt_read = 0;
   stream = svn_stream_create(baton, pool);
   svn_stream_set_read(stream, read_handler_string);
+  svn_stream_set_readline(stream, stream_readline);
+
   return stream;
 }
 
Index: subversion/libsvn_client/patch.c
===================================================================
--- subversion/libsvn_client/patch.c	(revision 961349)
+++ subversion/libsvn_client/patch.c	(working copy)
@@ -530,9 +530,10 @@ read_line(patch_target_t *target,
       APR_ARRAY_PUSH(target->lines, svn_stream_mark_t *) = mark;
     }
 
+  eol_str = NULL;
   SVN_ERR(svn_stream_readline_detect_eol(target->stream, &line_raw,
                                          &eol_str, &target->eof,
-                                         scratch_pool));
+                                         scratch_pool, scratch_pool));
   if (target->eol_style == svn_subst_eol_style_none)
     target->eol_str = eol_str;
 
@@ -632,7 +633,8 @@ match_hunk(svn_boolean_t *matched, patch_target_t
 
       SVN_ERR(svn_stream_readline_detect_eol(hunk->original_text,
                                              &hunk_line, NULL,
-                                             &hunk_eof, iterpool));
+                                             &hunk_eof,
+                                             iterpool, iterpool));
       /* Contract keywords, if any, before matching. */
       SVN_ERR(svn_subst_translate_cstring2(hunk_line->data,
                                            &hunk_line_translated,
@@ -676,7 +678,8 @@ match_hunk(svn_boolean_t *matched, patch_target_t
       /* If the target has no newline at end-of-file, we get an EOF
        * indication for the target earlier than we do get it for the hunk. */
       SVN_ERR(svn_stream_readline_detect_eol(hunk->original_text, &hunk_line,
-                                             NULL, &hunk_eof, iterpool));
+                                             NULL, &hunk_eof,
+                                             iterpool, iterpool));
       if (hunk_line->len == 0 && hunk_eof)
         *matched = lines_matched;
       else
@@ -927,8 +930,10 @@ reject_hunk(patch_target_t *target, hunk_info_t *h
 
       svn_pool_clear(iterpool);
 
+      eol_str = NULL;
       SVN_ERR(svn_stream_readline_detect_eol(hi->hunk->diff_text, &hunk_line,
-                                             &eol_str, &eof, iterpool));
+                                             &eol_str, &eof, iterpool,
+                                             iterpool));
       if (! eof)
         {
           if (hunk_line->len >= 1)
@@ -992,9 +997,10 @@ apply_hunk(patch_target_t *target, hunk_info_t *hi
 
       svn_pool_clear(iterpool);
 
+      eol_str = NULL;
       SVN_ERR(svn_stream_readline_detect_eol(hi->hunk->modified_text,
                                              &hunk_line, &eol_str,
-                                             &eof, iterpool));
+                                             &eof, iterpool, iterpool));
       lines_read++;
       if (! eof && lines_read > hi->fuzz &&
           lines_read <= hi->hunk->modified_length - hi->fuzz)
Index: subversion/tests/libsvn_subr/stream-test.c
===================================================================
--- subversion/tests/libsvn_subr/stream-test.c	(revision 961349)
+++ subversion/tests/libsvn_subr/stream-test.c	(working copy)
@@ -307,143 +307,7 @@ test_stream_range(apr_pool_t *pool)
     return SVN_NO_ERROR;
 }
 
-/* An implementation of svn_io_line_filter_cb_t */
 static svn_error_t *
-line_filter(svn_boolean_t *filtered, const char *line, void *baton,
-            apr_pool_t *scratch_pool)
-{
-  *filtered = strchr(line, '!') != NULL;
-  return SVN_NO_ERROR;
-}
-
-static svn_error_t *
-test_stream_line_filter(apr_pool_t *pool)
-{
-  static const char *lines[4] = {"Not filtered.", "Filtered!",
-                                 "Not filtered either.", "End of the lines!"};
-  svn_string_t *string;
-  svn_stream_t *stream;
-  svn_stringbuf_t *line;
-  svn_boolean_t eof;
-
-  string = svn_string_createf(pool, "%s\n%s\n%s\n%s", lines[0], lines[1],
-                              lines[2], lines[3]);
-  stream = svn_stream_from_string(string, pool);
-
-  svn_stream_set_line_filter_callback(stream, line_filter);
-
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, lines[0]);
-  /* line[1] should be filtered */
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, lines[2]);
-
-  /* The last line should also be filtered, and the resulting
-   * stringbuf should be empty. */
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_ASSERT(eof && svn_stringbuf_isempty(line));
-
-  return SVN_NO_ERROR;
-}
-
-/* An implementation of svn_io_line_transformer_cb_t */
-static svn_error_t *
-line_transformer(svn_stringbuf_t **buf, const char *line, void *baton,
-                 apr_pool_t *result_pool, apr_pool_t *scratch_pool)
-{
-  int i, len = strlen(line);
-  char *temp = apr_palloc(scratch_pool, len + 1 );
-
-  for (i = 0; i < len; i++)
-    {
-      temp[i] = line[len - 1 - i];
-    }
-
-  temp[len] = '\0';
-
-  *buf = svn_stringbuf_create(temp, result_pool);
-
-  return SVN_NO_ERROR;
-}
-
-static svn_error_t *
-test_stream_line_transformer(apr_pool_t *pool)
-{
-  static const char *lines[4] = {"gamma", "",
-                                 "iota", "!"};
-
-  static const char *inv_lines[4] = {"ammag", "",
-                                 "atoi", "!"};
-  svn_string_t *string;
-  svn_stream_t *stream;
-  svn_stringbuf_t *line;
-  svn_boolean_t eof;
-
-  string = svn_string_createf(pool, "%s\n%s\n%s\n%s", lines[0], lines[1],
-                              lines[2], lines[3]);
-
-  stream = svn_stream_from_string(string, pool);
-
-  svn_stream_set_line_transformer_callback(stream, line_transformer);
-
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, inv_lines[0]);
-
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, inv_lines[1]);
-
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, inv_lines[2]);
-
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, inv_lines[3]);
-
-  /* We should have reached eof and the stringbuf should be emtpy. */
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_ASSERT(eof && svn_stringbuf_isempty(line));
-
-  return SVN_NO_ERROR;
-}
-
-static svn_error_t *
-test_stream_line_filter_and_transformer(apr_pool_t *pool)
-{
-  static const char *lines[4] = {"!gamma", "",
-                                 "iota", "!"};
-
-  static const char *inv_lines[4] = {"ammag", "",
-                                 "atoi", "!"};
-  svn_string_t *string;
-  svn_stream_t *stream;
-  svn_stringbuf_t *line;
-  svn_boolean_t eof;
-
-  string = svn_string_createf(pool, "%s\n%s\n%s\n%s", lines[0], lines[1],
-                              lines[2], lines[3]);
-
-  stream = svn_stream_from_string(string, pool);
-
-  svn_stream_set_line_filter_callback(stream, line_filter);
-
-  svn_stream_set_line_transformer_callback(stream, line_transformer);
-
-  /* Line one should be filtered. */
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, inv_lines[1]);
-
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, inv_lines[2]);
-
-  /* The last line should also be filtered, and the resulting
-   * stringbuf should be empty. */
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_ASSERT(eof && svn_stringbuf_isempty(line));
-
-  return SVN_NO_ERROR;
-
-}
-
-static svn_error_t *
 test_stream_tee(apr_pool_t *pool)
 {
   svn_stringbuf_t *test_bytes = generate_test_bytes(100, pool);
@@ -647,12 +511,6 @@ struct svn_test_descriptor_t test_funcs[] =
                    "test compressed streams"),
     SVN_TEST_PASS2(test_stream_range,
                    "test streams reading from range of file"),
-    SVN_TEST_PASS2(test_stream_line_filter,
-                   "test stream line filtering"),
-    SVN_TEST_PASS2(test_stream_line_transformer,
-                   "test stream line transforming"),
-    SVN_TEST_PASS2(test_stream_line_filter_and_transformer,
-                   "test stream line filtering and transforming"),
     SVN_TEST_PASS2(test_stream_tee,
                    "test 'tee' streams"),
     SVN_TEST_PASS2(test_stream_seek_file,

Re: [PATCH] issue #3555: make svn_stream_readline() a stream method

Posted by Stefan Sperling <st...@elego.de>.
On Thu, Jul 08, 2010 at 09:52:51AM +0100, Julian Foad wrote:
> Hi Stefan.  I've just started looking at this.  Overall impression is it
> looks like you've done it very well - thank you!
> 
> Could you possibly get the readline_detect_eol() API changes out of this
> patch, and make them in a separate patch either before or after this?
> Or if that change belongs in this patch, please explain why in the log
> msg.

It's actually not needed. I think I added it trying to make the
parameter list of svn_io_readline_fn_t match the ones of
svn_stream_readline and svn_stream_readline_detect_eol as closely
as possible.

Looking at it again, adding another parameter to svn_io_readline_fn_t
which says whether the EOL should be detected is much cleaner.
This simplifies implementations, and allows callers of
svn_stream_readline_detect_eol to be lazy about initialising the
eol output parameter.

Updated diff (verified by running just the patch tests which exercise
stream_readline a lot) and updated log message below. Thanks!

[[[
Fix issue #3555, 'Remove the "line_filter" and "line_transformer" callbacks
from svn_stream_t'.

Make svn_stream_readline() a virtual method of svn_stream_t,
and provide custom implementations of readline methods in the diff
parsing code (which is the only place where we need this right now).

* subversion/include/svn_io.h
  (svn_io_line_filter_cb_t, svn_io_line_transformer_cb_t,
   svn_stream_set_line_filter_callback,
   svn_stream_set_line_transformer_callback): Remove.
  (svn_io_readline_fn_t, svn_stream_set_readline): Declare.
  (svn_stream_readline): Adjust docstring.
  (svn_stream_readline_detect_eol): Switch to dual-pool paradigm and
   document that the stream needs to support mark and seek.

* subversion/libsvn_diff/parse-diff.c
  (original_line_filter, modified_line_filter, remove_leading_char_transformer,
   reverse_diff_transformer): Remove.
  (hunk_text_stream_baton, read_handler_hunk_text, write_handler_hunk_text,
   close_handler_hunk_text, reset_handler_hunk_text, mark_handler_hunk_text,
   seek_handler_hunk_text, scan_eol, readline_handler_hunk_text,
   stream_hunk_text, stream_hunk_text_original, stream_hunk_text_modified):
    New. Implementation of a stream which provides a special readline
    method to read original/modified texts of hunks from a patch file stream.
  (reverse_diff_text_stream_baton, read_handler_reverse_diff_text,
   write_handler_reverse_diff_text, close_handler_reverse_diff_text,
   reset_handler_reverse_diff_text, mark_handler_reverse_diff_text,
   seek_handler_reverse_diff_text, readline_handler_reverse_diff_text,
   stream_reverse_diff_text): New. Implementation of a stream which
    provides a special readline method which reverses unidiff data read
    from the wrapped stream.
  (parse_next_hunk): Track svn_stream_readline_detect_eol() dual-pool change.
   Add a comment explaining why the patch file gets opened multiple
   times (drive-by fix because this confused me at first).
   Instead of installing line-filter/transformation callbacks on
   streams, wrap streams with appropriate wrapper streams.

* subversion/libsvn_subr/stream.c
  (struct svn_stream_t): Replace line_filter_cb and line_transformer_cb
   members with readline_fn member.
  (svn_stream_create): Track changes to svn_stream_t.
  (svn_stream_set_line_filter_callback,
   svn_stream_set_line_transformer_callback,
   line_filter, line_transformer): Remove.
  (scan_eol): Tweak argument list for use within a stream method.
   This function can no longer expect a stream, so pass a baton
   and a set of required stream methods instead.
  (stream_readline): Make this an svn_io_readline_fn_t implementation.
   Remove handling of line filters/transformers.
  (svn_stream_readline): Instead of calling the stream_readline() helper
   directly, call a custom readline implementation if one is set on the
   stream. If no custom implementation is provided, fall back to the
   stream_readline() helper function to preserve compatibility with 1.6.x.
  (svn_stream_readline_detect_eol): As previous, and ensure that
   the stream has mark/seek support as it is needed for EOL detection.
  (readline_handler_empty): Custom readline handler for the empty stream.
  (svn_stream_empty, svn_stream_disown, svn_stream_from_aprfile2,
   svn_stream_from_aprfile_range_readonly, svn_stream_compressed,
   svn_stream_checksummed2, svn_stream_checksummed, svn_stream_from_stringbuf,
   svn_stream_from_string): Set a readline method.

* subversion/libsvn_client/patch.c
  (read_line, match_hunk, reject_hunk, apply_hunk): Pass two pools to
   svn_stream_readline_detect_eol().

* subversion/tests/libsvn_subr/stream-test.c
  (line_filter, test_stream_line_filter, line_transformer,
   test_stream_line_transformer,
   test_stream_line_filter_and_transformer): Remove these tests.
  (test_funcs): Remove removed tests.
]]]


Index: subversion/include/svn_io.h
===================================================================
--- subversion/include/svn_io.h	(revision 961349)
+++ subversion/include/svn_io.h	(working copy)
@@ -780,40 +780,39 @@ typedef svn_error_t *(*svn_io_mark_fn_t)(void *bat
 typedef svn_error_t *(*svn_io_seek_fn_t)(void *baton,
                                          svn_stream_mark_t *mark);
 
-/** Line-filtering callback function for a generic stream.
- * @a baton is the stream's baton.
- * @see svn_stream_t, svn_stream_set_baton() and svn_stream_readline().
+/** Line-reading handler function for a generic stream.
  *
- * @since New in 1.7.
- */
-typedef svn_error_t *(*svn_io_line_filter_cb_t)(svn_boolean_t *filtered,
-                                                const char *line,
-                                                void *baton,
-                                                apr_pool_t *scratch_pool);
-
-/** A callback function, invoked by svn_stream_readline(), which can perform
- * arbitary transformations on the line before it is passed back to the caller
- * of svn_stream_readline().
+ * Allocate @a *stringbuf in @a result_pool, and read into it one line
+ * from the stream. @a baton is the stream's baton.
+ * If @a detect_eol is @c FALSE, @a *eol is used as the expected line
+ * terminator. If @a detect_eol is @c TRUE, the line-terminator is
+ * detected automatically and stored in @a *eol if @a eol is not NULL.
+ * If EOF is reached and the stream does not end with a newline character,
+ * and @a eol is not NULL, @a *eol is set to NULL.
  *
- * Returns a transformed stringbuf in @a buf, allocated in @a result_pool.
- * This callback gets invoked on lines which were not filtered by the
- * line-filtering callback function.
+ * @a mark_fn and @a seek_fn are required to be non-NULL if the end-of-line
+ * indicator is to be detected automatically. Else, they may be NULL.
  *
- * Implementations should always at least return an empty stringbuf.
- * It is a fatal error if an implementation returns @a *buf as NULL.
+ * The line-terminator is read from the stream, but is not added to
+ * the end of the stringbuf.  Instead, the stringbuf ends with a usual '\\0'.
  *
- * @a baton is the stream's baton.
+ * If @a stream runs out of bytes before encountering a line-terminator,
+ * then set @a *eof to @c TRUE, otherwise set @a *eof to @c FALSE.
  *
- * @see svn_stream_t, svn_stream_set_baton(), svn_io_line_filter_cb_t and
- * svn_stream_readline().
+ * Temporary allocations will be performed in @a scratch_pool.
  *
- * @since New in 1.7.
- */
-typedef svn_error_t *(*svn_io_line_transformer_cb_t)(svn_stringbuf_t **buf,
-                                                     const char *line,
-                                                     void *baton,
-                                                     apr_pool_t *result_pool,
-                                                     apr_pool_t *scratch_pool);
+ * @see svn_stream_readline and svn_stream_readline_detect_eol.
+ * @since New in 1.7. */
+typedef svn_error_t *(*svn_io_readline_fn_t)(void *baton,
+                                             svn_stringbuf_t **stringbuf,
+                                             const char **eol,
+                                             svn_boolean_t *eof,
+                                             svn_boolean_t detect_eol,
+                                             svn_read_fn_t read_fn,
+                                             svn_io_mark_fn_t mark_fn,
+                                             svn_io_seek_fn_t seek_fn,
+                                             apr_pool_t *result_pool,
+                                             apr_pool_t *scratch_pool);
 
 /** Create a generic stream.  @see svn_stream_t. */
 svn_stream_t *
@@ -864,23 +863,11 @@ void
 svn_stream_set_seek(svn_stream_t *stream,
                     svn_io_seek_fn_t seek_fn);
 
-/** Set @a stream's line-filtering callback function to @a line_filter_cb
- *
- * @since New in 1.7.
- */
+/** Set @a stream's readline function to @a readline_fn */
 void
-svn_stream_set_line_filter_callback(svn_stream_t *stream,
-                                    svn_io_line_filter_cb_t line_filter_cb);
+svn_stream_set_readline(svn_stream_t *stream,
+                        svn_io_readline_fn_t readline_fn);
 
-/** Set @a streams's line-transforming callback function to
- * @a line_transformer_cb.
- *
- * @since New in 1.7.
- */
-void
-svn_stream_set_line_transformer_callback(
-  svn_stream_t *stream,
-  svn_io_line_transformer_cb_t line_transformer_cb);
 
 /** Create a stream that is empty for reading and infinite for writing. */
 svn_stream_t *
@@ -1203,19 +1190,6 @@ svn_stream_printf_from_utf8(svn_stream_t *stream,
  *
  * If @a stream runs out of bytes before encountering a line-terminator,
  * then set @a *eof to @c TRUE, otherwise set @a *eof to FALSE.
- *
- * If a line-filter callback function was set on the stream using
- * svn_stream_set_line_filter_callback(), lines will only be returned
- * if they pass the filtering decision of the callback function.
- * If end-of-file is encountered while reading the line and the line
- * is filtered, @a *stringbuf will be empty.
- *
- * If a line-transformer callback function was set on the stream using
- * svn_stream_set_line_transformer_callback(), lines will be returned
- * transformed, in a way determined by the callback.
- *
- * Note that the line-transformer callback gets called after the line-filter
- * callback, not before.
  */
 svn_error_t *
 svn_stream_readline(svn_stream_t *stream,
@@ -1230,6 +1204,11 @@ svn_stream_readline(svn_stream_t *stream,
  * is returned in @a *eol.  If EOF is reached and the stream does not
  * end with a newline character, @a *eol will be NULL.
  *
+ * The @a stream is required to support mark and seek. @see svn_stream_mark.
+ *
+ * The @a *stringbuf will be allocated in @a result_pool.
+ * @a scratch_pool is used for temporary allocations.
+ *
  * @since New in 1.7.
  */
 svn_error_t *
@@ -1237,7 +1216,8 @@ svn_stream_readline_detect_eol(svn_stream_t *strea
                                svn_stringbuf_t **stringbuf,
                                const char **eol,
                                svn_boolean_t *eof,
-                               apr_pool_t *pool);
+                               apr_pool_t *result_pool,
+                               apr_pool_t *scratch_pool);
 
 /**
  * Read the contents of the readable stream @a from and write them to the
Index: subversion/libsvn_diff/parse-diff.c
===================================================================
--- subversion/libsvn_diff/parse-diff.c	(revision 961349)
+++ subversion/libsvn_diff/parse-diff.c	(working copy)
@@ -33,6 +33,8 @@
 #include "svn_dirent_uri.h"
 #include "svn_diff.h"
 
+#include "private/svn_eol_private.h"
+
 /* Helper macro for readability */
 #define starts_with(str, start)  \
   (strncmp((str), (start), strlen(start)) == 0)
@@ -184,89 +186,405 @@ parse_hunk_header(const char *header, svn_hunk_t *
   return TRUE;
 }
 
-/* A stream line-filter which allows only original text from a hunk,
- * and filters special lines (which start with a backslash). */
+/* Users of the diff parsing API are provided with various streams
+ * to read lines from a hunk. These streams return:
+ *
+ *  - the original hunk text (all lines starting with '-' or ' ')
+ *  - the modified hunk text (all lines starting with '+' or ' ')
+ *  - the plain unidiff text (possibly reversed)
+ *
+ * To achieve this, we wrap the patch file stream with custom streams,
+ * which override the wrapped stream's readline method. */
+
+/* Baton for a stream that reads hunk texts. */
+struct hunk_text_stream_baton {
+  /* The leading unidiff symbol of lines which should be filtered.
+   * Can be '+' or '-', depending on whether we're providing the original
+   * or modified version of the hunk. */
+  char verboten;
+
+  svn_stream_t *wrapped_stream;
+};
+
+/* An implementation of svn_read_fn_t. */
 static svn_error_t *
-original_line_filter(svn_boolean_t *filtered, const char *line, void *baton,
-                     apr_pool_t *scratch_pool)
+read_handler_hunk_text(void *baton, char *buffer, apr_size_t *len)
 {
-  *filtered = (line[0] == '+' || line[0] == '\\');
-  return SVN_NO_ERROR;
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_read(b->wrapped_stream, buffer, len);
 }
 
-/* A stream line-filter which allows only modified text from a hunk,
- * and filters special lines (which start with a backslash). */
+/* An implementation of svn_write_fn_t. */
 static svn_error_t *
-modified_line_filter(svn_boolean_t *filtered, const char *line, void *baton,
-                     apr_pool_t *scratch_pool)
+write_handler_hunk_text(void *baton, const char *buffer, apr_size_t *len)
 {
-  *filtered = (line[0] == '-' || line[0] == '\\');
-  return SVN_NO_ERROR;
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_write(b->wrapped_stream, buffer, len);
 }
 
-/** line-transformer callback to shave leading diff symbols. */
+/* An implementation of svn_close_fn_t. */
 static svn_error_t *
-remove_leading_char_transformer(svn_stringbuf_t **buf,
-                                const char *line,
-                                void *baton,
-                                apr_pool_t *result_pool,
-                                apr_pool_t *scratch_pool)
+close_handler_hunk_text(void *baton)
 {
-  if (line[0] == '+' || line[0] == '-' || line[0] == ' ')
-    *buf = svn_stringbuf_create(line + 1, result_pool);
-  else
-    *buf = svn_stringbuf_create(line, result_pool);
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_close(b->wrapped_stream);
+}
 
+/* An implementation of svn_io_reset_fn_t. */
+static svn_error_t *
+reset_handler_hunk_text(void *baton)
+{
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_reset(b->wrapped_stream);
+}
+
+/* An implementation of svn_io_mark_fn_t. */
+static svn_error_t *
+mark_handler_hunk_text(void *baton, svn_stream_mark_t **mark, apr_pool_t *pool)
+{
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_mark(b->wrapped_stream, mark, pool);
+}
+
+/* An implementation of svn_io_seek_fn_t. */
+static svn_error_t *
+seek_handler_hunk_text(void *baton, svn_stream_mark_t *mark)
+{
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_seek(b->wrapped_stream, mark);
+}
+
+/* Invoke the READ_FN function of a stream to scan for an end-of-line
+ * indicator, and return it in *EOL.
+ * Set *EOL to NULL if the stream runs out before an end-of-line indicator
+ * is found. */
+static svn_error_t *
+scan_eol(void *baton,
+         svn_read_fn_t read_fn,
+         svn_io_mark_fn_t mark_fn,
+         svn_io_seek_fn_t seek_fn,
+         const char **eol,
+         apr_pool_t *pool)
+{
+  const char *eol_str;
+  svn_stream_mark_t *mark;
+
+  SVN_ERR(mark_fn(baton, &mark, pool));
+
+  eol_str = NULL;
+  while (! eol_str)
+    {
+      char buf[512];
+      apr_size_t len;
+
+      len = sizeof(buf);
+      SVN_ERR(read_fn(baton, buf, &len));
+      if (len == 0)
+          break; /* EOF */
+      eol_str = svn_eol__detect_eol(buf, buf + len);
+    }
+
+  SVN_ERR(seek_fn(baton, mark));
+
+  *eol = eol_str;
+
   return SVN_NO_ERROR;
 }
 
-/** line-transformer callback to reverse a diff text. */
+/* An implementation of svn_io_readline_fn_t. */
 static svn_error_t *
-reverse_diff_transformer(svn_stringbuf_t **buf,
-                         const char *line,
-                         void *baton,
-                         apr_pool_t *result_pool,
-                         apr_pool_t *scratch_pool)
+readline_handler_hunk_text(void *baton,
+                           svn_stringbuf_t **stringbuf,
+                           const char **eol,
+                           svn_boolean_t *eof,
+                           svn_boolean_t detect_eol,
+                           svn_read_fn_t read_fn,
+                           svn_io_mark_fn_t mark_fn,
+                           svn_io_seek_fn_t seek_fn,
+                           apr_pool_t *result_pool,
+                           apr_pool_t *scratch_pool)
 {
-  svn_hunk_t hunk;
+  svn_stringbuf_t *str;
+  apr_pool_t *iterpool;
+  svn_boolean_t filtered;
+  const char *eol_str;
+  struct hunk_text_stream_baton *b = baton;
 
-  /* ### Pass the already parsed hunk via the baton?
-   * ### Maybe we should really make svn_stream_readline() a proper stream
-   * ### method and override it instead of adding special callbacks? */
-  if (parse_hunk_header(line, &hunk, "@@", FALSE, scratch_pool))
+  *eof = FALSE;
+
+  iterpool = svn_pool_create(scratch_pool);
+  do
     {
-      *buf = svn_stringbuf_createf(result_pool,
-                                   "@@ -%lu,%lu +%lu,%lu @@",
-                                   hunk.modified_start,
-                                   hunk.modified_length,
-                                   hunk.original_start,
-                                   hunk.original_length);
+      apr_size_t numbytes;
+      const char *match;
+      char c;
+
+      svn_pool_clear(iterpool);
+
+      /* Since we're reading one character at a time, let's at least
+         optimize for the 90% case.  90% of the time, we can avoid the
+         stringbuf ever having to realloc() itself if we start it out at
+         80 chars.  */
+      str = svn_stringbuf_create_ensure(80, iterpool);
+
+      if (detect_eol)
+        {
+          SVN_ERR(scan_eol(baton, read_fn, mark_fn, seek_fn,
+                           &eol_str, iterpool));
+          if (eol)
+            *eol = eol_str;
+          if (eol_str == NULL)
+            {
+              /* No newline until EOF, EOL_STR can be anything. */
+              eol_str = APR_EOL_STR;
+            }
+        }
+      else
+        eol_str = *eol;
+
+      /* Read into STR up to and including the next EOL sequence. */
+      match = eol_str;
+      numbytes = 1;
+      while (*match)
+        {
+          SVN_ERR(read_fn(baton, &c, &numbytes));
+          if (numbytes != 1)
+            {
+              /* a 'short' read means the stream has run out. */
+              *eof = TRUE;
+              /* We know we don't have a whole EOL sequence, but ensure we
+               * don't chop off any partial EOL sequence that we may have. */
+              match = eol_str;
+              /* Process this short (or empty) line just like any other
+               * except with *EOF set. */
+              break;
+            }
+
+          if (c == *match)
+            match++;
+          else
+            match = eol_str;
+
+          svn_stringbuf_appendbytes(str, &c, 1);
+        }
+
+      svn_stringbuf_chop(str, match - eol_str);
+      filtered = (str->data[0] == b->verboten || str->data[0] == '\\');
     }
-  else if (parse_hunk_header(line, &hunk, "##", FALSE, scratch_pool))
+  while (filtered && ! *eof);
+  /* Not destroying the iterpool just yet since we still need STR
+   * which is allocated in it. */
+
+  if (filtered)
     {
-      *buf = svn_stringbuf_createf(result_pool,
-                                   "## -%lu,%lu +%lu,%lu ##",
-                                   hunk.modified_start,
-                                   hunk.modified_length,
-                                   hunk.original_start,
-                                   hunk.original_length);
+      /* EOF, return an empty string. */
+      *stringbuf = svn_stringbuf_create_ensure(0, result_pool);
     }
-  else if (line[0] == '+')
+  else if (str->data[0] == '+' || str->data[0] == '-' || str->data[0] == ' ')
     {
-      *buf = svn_stringbuf_create(line, result_pool);
-      (*buf)->data[0] = '-';
+      /* Shave off leading unidiff symbols. */
+      *stringbuf = svn_stringbuf_create(str->data + 1, result_pool);
     }
-  else if (line[0] == '-')
+  else
     {
-      *buf = svn_stringbuf_create(line, result_pool);
-      (*buf)->data[0] = '+';
+      /* Return the line as-is. */
+      *stringbuf = svn_stringbuf_dup(str, result_pool);
     }
+
+  /* Done. RIP iterpool. */
+  svn_pool_destroy(iterpool);
+
+  return SVN_NO_ERROR;
+}
+
+/* Return a stream for reading hunk text from WRAPPED_STREAM.
+ * VERBOTEN is the leading character of lines which should be
+ * filtered by this stream's readline method ('+' or '-').
+ * Allocate the new stream in RESULT_POOL. */
+static svn_stream_t *
+stream_hunk_text(svn_stream_t *wrapped_stream,
+                 char verboten,
+                 apr_pool_t *result_pool)
+{
+  svn_stream_t *stream;
+  struct hunk_text_stream_baton *baton;
+
+  baton = apr_palloc(result_pool, sizeof(*baton));
+  baton->wrapped_stream = wrapped_stream;
+  baton->verboten = verboten;
+
+  stream = svn_stream_create(baton, result_pool);
+
+  svn_stream_set_read(stream, read_handler_hunk_text);
+  svn_stream_set_write(stream, write_handler_hunk_text);
+  svn_stream_set_close(stream, close_handler_hunk_text);
+  svn_stream_set_reset(stream, reset_handler_hunk_text);
+  svn_stream_set_mark(stream, mark_handler_hunk_text);
+  svn_stream_set_seek(stream, seek_handler_hunk_text);
+  svn_stream_set_readline(stream, readline_handler_hunk_text);
+
+  return stream;
+}
+
+/* Return a stream to read the original text of a hunk from
+ * WRAPPED_STREAM, and allocated in RESULT_POOL.*/
+static svn_stream_t *
+stream_hunk_text_original(svn_stream_t *wrapped_stream,
+                          apr_pool_t *result_pool)
+{
+  return stream_hunk_text(wrapped_stream, '+', result_pool);
+}
+
+/* Return a stream to read the modified text of a hunk from
+ * WRAPPED_STREAM, and allocated in RESULT_POOL.*/
+static svn_stream_t *
+stream_hunk_text_modified(svn_stream_t *wrapped_stream,
+                          apr_pool_t *result_pool)
+{
+  return stream_hunk_text(wrapped_stream, '-', result_pool);
+}
+
+
+/* Baton for a stream that reads unidiff text reversed. */
+struct reverse_diff_text_stream_baton {
+  svn_stream_t *wrapped_stream;
+}; 
+
+/* An implementation of svn_read_fn_t. */
+static svn_error_t *
+read_handler_reverse_diff_text(void *baton, char *buffer, apr_size_t *len)
+{
+  struct reverse_diff_text_stream_baton *b = baton;
+  return svn_stream_read(b->wrapped_stream, buffer, len);
+}
+
+/* An implementation of svn_write_fn_t. */
+static svn_error_t *
+write_handler_reverse_diff_text(void *baton, const char *buffer,
+                                apr_size_t *len)
+{
+  struct reverse_diff_text_stream_baton *b = baton;
+  return svn_stream_write(b->wrapped_stream, buffer, len);
+}
+
+/* An implementation of svn_close_fn_t. */
+static svn_error_t *
+close_handler_reverse_diff_text(void *baton)
+{
+  struct reverse_diff_text_stream_baton *b = baton;
+  return svn_stream_close(b->wrapped_stream);
+}
+
+/* An implementation of svn_io_reset_fn_t. */
+static svn_error_t *
+reset_handler_reverse_diff_text(void *baton)
+{
+  struct reverse_diff_text_stream_baton *b = baton;
+  return svn_stream_reset(b->wrapped_stream);
+}
+
+/* An implementation of svn_io_mark_fn_t. */
+static svn_error_t *
+mark_handler_reverse_diff_text(void *baton, svn_stream_mark_t **mark,
+                               apr_pool_t *pool)
+{
+  struct reverse_diff_text_stream_baton *b = baton;
+  return svn_stream_mark(b->wrapped_stream, mark, pool);
+}
+
+/* An implementation of svn_io_seek_fn_t. */
+static svn_error_t *
+seek_handler_reverse_diff_text(void *baton, svn_stream_mark_t *mark)
+{
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_seek(b->wrapped_stream, mark);
+}
+
+/* An implementation of svn_io_readline_fn_t. */
+static svn_error_t *
+readline_handler_reverse_diff_text(void *baton,
+                                   svn_stringbuf_t **stringbuf,
+                                   const char **eol,
+                                   svn_boolean_t *eof,
+                                   svn_boolean_t detect_eol,
+                                   svn_read_fn_t read_fn,
+                                   svn_io_mark_fn_t mark_fn,
+                                   svn_io_seek_fn_t seek_fn,
+                                   apr_pool_t *result_pool,
+                                   apr_pool_t *scratch_pool)
+{
+  svn_hunk_t hunk;
+  svn_stringbuf_t *line;
+  struct reverse_diff_text_stream_baton *b = baton;
+
+  /* Read the line and perform necessary transformations to
+   * produce a reversed diff. */
+  if (detect_eol)
+    SVN_ERR(svn_stream_readline_detect_eol(b->wrapped_stream,
+                                           &line, eol, eof,
+                                           result_pool, scratch_pool));
   else
-    *buf = svn_stringbuf_create(line, result_pool);
+    SVN_ERR(svn_stream_readline(b->wrapped_stream, &line, *eol, eof,
+                                result_pool));
 
+  if (parse_hunk_header(line->data, &hunk, "@@", FALSE, scratch_pool))
+    {
+      /* Line is a hunk header, reverse it. */
+      *stringbuf = svn_stringbuf_createf(result_pool,
+                                         "@@ -%lu,%lu +%lu,%lu @@",
+                                         hunk.modified_start,
+                                         hunk.modified_length,
+                                         hunk.original_start,
+                                         hunk.original_length);
+    }
+  else if (parse_hunk_header(line->data, &hunk, "##", FALSE, scratch_pool))
+    {
+      /* Line is a hunk header, reverse it. */
+      *stringbuf = svn_stringbuf_createf(result_pool,
+                                         "## -%lu,%lu +%lu,%lu ##",
+                                         hunk.modified_start,
+                                         hunk.modified_length,
+                                         hunk.original_start,
+                                         hunk.original_length);
+    }
+  else
+    {
+      if (line->data[0] == '+')
+        line->data[0] = '-';
+      else if (line->data[0] == '-')
+        line->data[0] = '+';
+
+      *stringbuf = line;
+    }
+
   return SVN_NO_ERROR;
 }
 
+/* Return a stream for reading diff text from WRAPPED_STREAM.
+ * The unidiff will appear reversed when read via the stream's readline method.
+ * Allocate the new stream in RESULT_POOL. */
+static svn_stream_t *
+stream_reverse_diff_text(svn_stream_t *wrapped_stream,
+                         apr_pool_t *result_pool)
+{
+  svn_stream_t *stream;
+  struct reverse_diff_text_stream_baton *baton;
+
+  baton = apr_palloc(result_pool, sizeof(*baton));
+  baton->wrapped_stream = wrapped_stream;
+
+  stream = svn_stream_create(baton, result_pool);
+  svn_stream_set_read(stream, read_handler_reverse_diff_text);
+  svn_stream_set_write(stream, write_handler_reverse_diff_text);
+  svn_stream_set_close(stream, close_handler_reverse_diff_text);
+  svn_stream_set_reset(stream, reset_handler_reverse_diff_text);
+  svn_stream_set_mark(stream, mark_handler_reverse_diff_text);
+  svn_stream_set_seek(stream, seek_handler_reverse_diff_text);
+  svn_stream_set_readline(stream, readline_handler_reverse_diff_text);
+
+  return stream;
+}
+
 /* Parse PROP_NAME from HEADER as the part after the INDICATOR line. */
 static svn_error_t *
 parse_prop_name(const char **prop_name, const char *header, 
@@ -349,7 +667,7 @@ parse_next_hunk(svn_hunk_t **hunk,
       /* Remember the current line's offset, and read the line. */
       last_line = pos;
       SVN_ERR(svn_stream_readline_detect_eol(stream, &line, NULL, &eof,
-                                             iterpool));
+                                             iterpool, iterpool));
 
       if (! eof)
         {
@@ -502,38 +820,41 @@ parse_next_hunk(svn_hunk_t **hunk,
       apr_file_t *f;
       apr_int32_t flags = APR_READ | APR_BUFFERED;
 
+      /* For each of the streams created below, we re-open the patch file.
+       * Each stream needs its own file descriptor in order to have
+       * independent seek behaviour. */
+
       /* Create a stream which returns the hunk text itself. */
       SVN_ERR(svn_io_file_open(&f, patch->path, flags, APR_OS_DEFAULT,
                                result_pool));
       diff_text = svn_stream_from_aprfile_range_readonly(f, FALSE,
                                                          start, end,
                                                          result_pool);
+      if (reverse)
+        diff_text = stream_reverse_diff_text(diff_text, result_pool);
 
       /* Create a stream which returns the original hunk text. */
       SVN_ERR(svn_io_file_open(&f, patch->path, flags, APR_OS_DEFAULT,
                                result_pool));
-      original_text = svn_stream_from_aprfile_range_readonly(f, FALSE,
-                                                             start, end,
-                                                             result_pool);
-      svn_stream_set_line_filter_callback(original_text, original_line_filter);
-      svn_stream_set_line_transformer_callback(original_text,
-                                               remove_leading_char_transformer);
+      original_text = stream_hunk_text_original(
+                        svn_stream_from_aprfile_range_readonly(f, FALSE,
+                                                               start, end,
+                                                               result_pool),
+                        result_pool);
 
       /* Create a stream which returns the modified hunk text. */
       SVN_ERR(svn_io_file_open(&f, patch->path, flags, APR_OS_DEFAULT,
                                result_pool));
-      modified_text = svn_stream_from_aprfile_range_readonly(f, FALSE,
-                                                             start, end,
-                                                             result_pool);
-      svn_stream_set_line_filter_callback(modified_text, modified_line_filter);
-      svn_stream_set_line_transformer_callback(modified_text,
-                                               remove_leading_char_transformer);
+      modified_text = stream_hunk_text_modified(
+                        svn_stream_from_aprfile_range_readonly(f, FALSE,
+                                                               start, end,
+                                                               result_pool),
+                        result_pool);
+
       /* Set the hunk's texts. */
       (*hunk)->diff_text = diff_text;
       if (reverse)
         {
-          svn_stream_set_line_transformer_callback(diff_text,
-                                                   reverse_diff_transformer);
           (*hunk)->original_text = modified_text;
           (*hunk)->modified_text = original_text;
         }
@@ -918,7 +1239,7 @@ svn_diff_parse_next_patch(svn_patch_t **patch,
       /* Remember the current line's offset, and read the line. */
       last_line = pos;
       SVN_ERR(svn_stream_readline_detect_eol(stream, &line, NULL, &eof,
-                                             iterpool));
+                                             iterpool, iterpool));
 
       if (! eof)
         {
Index: subversion/libsvn_subr/stream.c
===================================================================
--- subversion/libsvn_subr/stream.c	(revision 961349)
+++ subversion/libsvn_subr/stream.c	(working copy)
@@ -53,8 +53,7 @@ struct svn_stream_t {
   svn_io_reset_fn_t reset_fn;
   svn_io_mark_fn_t mark_fn;
   svn_io_seek_fn_t seek_fn;
-  svn_io_line_filter_cb_t line_filter_cb;
-  svn_io_line_transformer_cb_t line_transformer_cb;
+  svn_io_readline_fn_t readline_fn;
 };
 
 
@@ -73,8 +72,7 @@ svn_stream_create(void *baton, apr_pool_t *pool)
   stream->reset_fn = NULL;
   stream->mark_fn = NULL;
   stream->seek_fn = NULL;
-  stream->line_filter_cb = NULL;
-  stream->line_transformer_cb = NULL;
+  stream->readline_fn = NULL;
   return stream;
 }
 
@@ -124,20 +122,11 @@ svn_stream_set_seek(svn_stream_t *stream, svn_io_s
 }
 
 void
-svn_stream_set_line_filter_callback(svn_stream_t *stream,
-                                    svn_io_line_filter_cb_t line_filter_cb)
+svn_stream_set_readline(svn_stream_t *stream, svn_io_readline_fn_t readline_fn)
 {
-  stream->line_filter_cb = line_filter_cb;
+  stream->readline_fn = readline_fn;
 }
 
-void
-svn_stream_set_line_transformer_callback(
-  svn_stream_t *stream,
-  svn_io_line_transformer_cb_t line_transformer_cb)
-{
-  stream->line_transformer_cb = line_transformer_cb;
-}
-
 svn_error_t *
 svn_stream_read(svn_stream_t *stream, char *buffer, apr_size_t *len)
 {
@@ -233,55 +222,23 @@ svn_stream_printf_from_utf8(svn_stream_t *stream,
   return svn_stream_write(stream, translated, &len);
 }
 
-/* If a line filter callback was set on STREAM, invoke it on LINE,
- * and indicate in *FILTERED whether the line should be filtered.
- * If no line filter callback was set on STREAM, just set *FILTERED to FALSE.
- */
-static svn_error_t *
-line_filter(svn_stream_t *stream, svn_boolean_t *filtered, const char *line,
-            apr_pool_t *pool)
-{
-  apr_pool_t *scratch_pool;
-
-  if (! stream->line_filter_cb)
-    {
-      *filtered = FALSE;
-      return SVN_NO_ERROR;
-    }
-
-  scratch_pool = svn_pool_create(pool);
-  SVN_ERR(stream->line_filter_cb(filtered, line, stream->baton, scratch_pool));
-  svn_pool_destroy(scratch_pool);
-  return SVN_NO_ERROR;
-}
-
-/* Run the line transformer callback of STREAM with LINE as input,
- * and expect the transformation result to be returned in BUF,
- * allocated in POOL. */
-static svn_error_t *
-line_transformer(svn_stream_t *stream, svn_stringbuf_t **buf,
-                 const char *line, apr_pool_t *pool, apr_pool_t *scratch_pool)
-{
-  *buf = NULL;  /* so we can assert that the callback has set it non-null */
-  SVN_ERR(stream->line_transformer_cb(buf, line, stream->baton,
-                                      pool, scratch_pool));
-
-  /* Die if the line transformer didn't provide any output. */
-  SVN_ERR_ASSERT(*buf);
-
-  return SVN_NO_ERROR;
-}
-
-/* Scan STREAM for an end-of-line indicatior, and return it in *EOL.
+/* Invoke the READ_FN function of a stream to scan  for an end-of-line
+ * indicatior, and return it in *EOL.
+ * Use MARK_FN and SEEK_FN to seek in the stream.
  * Set *EOL to NULL if the stream runs out before an end-of-line indicator
  * is found. */
 static svn_error_t *
-scan_eol(const char **eol, svn_stream_t *stream, apr_pool_t *pool)
+scan_eol(void *baton,
+         svn_read_fn_t read_fn,
+         svn_io_mark_fn_t mark_fn,
+         svn_io_seek_fn_t seek_fn,
+         const char **eol,
+         apr_pool_t *pool)
 {
   const char *eol_str;
   svn_stream_mark_t *mark;
 
-  SVN_ERR(svn_stream_mark(stream, &mark, pool));
+  SVN_ERR(mark_fn(baton, &mark, pool));
 
   eol_str = NULL;
   while (! eol_str)
@@ -290,113 +247,82 @@ static svn_error_t *
       apr_size_t len;
 
       len = sizeof(buf);
-      SVN_ERR(svn_stream_read(stream, buf, &len));
+      SVN_ERR(read_fn(baton, buf, &len));
       if (len == 0)
           break; /* EOF */
       eol_str = svn_eol__detect_eol(buf, buf + len);
     }
 
-  SVN_ERR(svn_stream_seek(stream, mark));
+  SVN_ERR(seek_fn(baton, mark));
 
   *eol = eol_str;
 
   return SVN_NO_ERROR;
 }
 
-/* Guts of svn_stream_readline() and svn_stream_readline_detect_eol().
- * Returns the line read from STREAM in *STRINGBUF, and indicates
- * end-of-file in *EOF.  If DETECT_EOL is TRUE, the end-of-line indicator
- * is detected automatically and returned in *EOL.
- * If DETECT_EOL is FALSE, *EOL must point to the desired end-of-line
- * indicator.  STRINGBUF is allocated in POOL. */
+/* An implementation of svn_io_readline_fn_t */
 static svn_error_t *
-stream_readline(svn_stringbuf_t **stringbuf,
-                svn_boolean_t *eof,
+stream_readline(void *baton,
+                svn_stringbuf_t **stringbuf,
                 const char **eol,
-                svn_stream_t *stream,
+                svn_boolean_t *eof,
                 svn_boolean_t detect_eol,
-                apr_pool_t *pool)
+                svn_read_fn_t read_fn,
+                svn_io_mark_fn_t mark_fn,
+                svn_io_seek_fn_t seek_fn,
+                apr_pool_t *result_pool,
+                apr_pool_t *scratch_pool)
 {
-  svn_stringbuf_t *str;
-  apr_pool_t *iterpool;
-  svn_boolean_t filtered;
+  apr_size_t numbytes;
+  const char *match;
+  char c;
   const char *eol_str;
+  /* Since we're reading one character at a time, let's at least
+     optimize for the 90% case.  90% of the time, we can avoid the
+     stringbuf ever having to realloc() itself if we start it out at
+     80 chars.  */
+  svn_stringbuf_t *str = svn_stringbuf_create_ensure(80, scratch_pool);
 
-  *eof = FALSE;
-
-  iterpool = svn_pool_create(pool);
-  do
+  if (detect_eol)
     {
-      apr_size_t numbytes;
-      const char *match;
-      char c;
-
-      svn_pool_clear(iterpool);
-
-      /* Since we're reading one character at a time, let's at least
-         optimize for the 90% case.  90% of the time, we can avoid the
-         stringbuf ever having to realloc() itself if we start it out at
-         80 chars.  */
-      str = svn_stringbuf_create_ensure(80, iterpool);
-
-      if (detect_eol)
+      SVN_ERR(scan_eol(baton, read_fn, mark_fn, seek_fn,
+                       &eol_str, scratch_pool));
+      if (eol)
+        *eol = eol_str;
+      if (eol_str == NULL)
         {
-          SVN_ERR(scan_eol(&eol_str, stream, iterpool));
-          if (eol)
-            *eol = eol_str;
-          if (! eol_str)
-            {
-              /* No newline until EOF, EOL_STR can be anything. */
-              eol_str = APR_EOL_STR;
-            }
+          /* No newline until EOF, EOL_STR can be anything. */
+          eol_str = APR_EOL_STR;
         }
-      else
-        eol_str = *eol;
+    }
+  else
+    eol_str = *eol;
 
-      /* Read into STR up to and including the next EOL sequence. */
-      match = eol_str;
+  /* Read into STR up to and including the next EOL sequence. */
+  match = eol_str;
+  while (*match)
+    {
       numbytes = 1;
-      while (*match)
+      SVN_ERR(read_fn(baton, &c, &numbytes));
+      if (numbytes != 1)
         {
-          SVN_ERR(svn_stream_read(stream, &c, &numbytes));
-          if (numbytes != 1)
-            {
-              /* a 'short' read means the stream has run out. */
-              *eof = TRUE;
-              /* We know we don't have a whole EOL sequence, but ensure we
-               * don't chop off any partial EOL sequence that we may have. */
-              match = eol_str;
-              /* Process this short (or empty) line just like any other
-               * except with *EOF set. */
-              break;
-            }
-
-          if (c == *match)
-            match++;
-          else
-            match = eol_str;
-
-          svn_stringbuf_appendbytes(str, &c, 1);
+          /* a 'short' read means the stream has run out. */
+          *eof = TRUE;
+          *stringbuf = svn_stringbuf_dup(str, result_pool);
+          return SVN_NO_ERROR;
         }
 
-      svn_stringbuf_chop(str, match - eol_str);
+      if (c == *match)
+        match++;
+      else
+        match = eol_str;
 
-      SVN_ERR(line_filter(stream, &filtered, str->data, iterpool));
+      svn_stringbuf_appendbytes(str, &c, 1);
     }
-  while (filtered && ! *eof);
-  /* Not destroying the iterpool just yet since we still need STR
-   * which is allocated in it. */
 
-  if (filtered)
-    *stringbuf = svn_stringbuf_create_ensure(0, pool);
-  else if (stream->line_transformer_cb)
-    SVN_ERR(line_transformer(stream, stringbuf, str->data, pool, iterpool));
-  else
-    *stringbuf = svn_stringbuf_dup(str, pool);
-
-  /* Done. RIP iterpool. */
-  svn_pool_destroy(iterpool);
-
+  *eof = FALSE;
+  svn_stringbuf_chop(str, match - eol_str);
+  *stringbuf = svn_stringbuf_dup(str, result_pool);
   return SVN_NO_ERROR;
 }
 
@@ -407,8 +333,23 @@ svn_stream_readline(svn_stream_t *stream,
                     svn_boolean_t *eof,
                     apr_pool_t *pool)
 {
-  return svn_error_return(stream_readline(stringbuf, eof, &eol, stream,
-                                          FALSE, pool));
+  svn_io_readline_fn_t readline_fn;
+
+  /* Provide a default readline implementation if the stream
+   * hasn't overridden it. This is needed for backwards compat
+   * to 1.6.x and earlier. */
+  if (stream->readline_fn)
+    readline_fn = stream->readline_fn;
+  else
+    readline_fn = stream_readline;
+
+  return svn_error_return(readline_fn(stream->baton,
+                                      stringbuf, &eol, eof,
+                                      FALSE,
+                                      stream->read_fn,
+                                      stream->mark_fn,
+                                      stream->seek_fn,
+                                      pool, pool));
 }
 
 svn_error_t *
@@ -416,13 +357,36 @@ svn_stream_readline_detect_eol(svn_stream_t *strea
                                svn_stringbuf_t **stringbuf,
                                const char **eol,
                                svn_boolean_t *eof,
-                               apr_pool_t *pool)
+                               apr_pool_t *result_pool,
+                               apr_pool_t *scratch_pool)
 {
-  return svn_error_return(stream_readline(stringbuf, eof, eol, stream,
-                                          TRUE, pool));
-}
+  svn_io_readline_fn_t readline_fn;
 
+  /* Provide a default readline implementation if the stream
+   * hasn't overridden it. This is not needed for backwards compat
+   * to 1.6.x and earlier (this function is new in 1.7), but it
+   * is nice anyway because it saves us from adding dummy readline
+   * methods to custom streams sprinkled throughout the code. */
+  if (stream->readline_fn)
+    readline_fn = stream->readline_fn;
+  else
+    readline_fn = stream_readline;
 
+  /* EOL-detection requires mark/seek support. */
+  if (stream->mark_fn == NULL)
+    return svn_error_create(SVN_ERR_STREAM_SEEK_NOT_SUPPORTED, NULL, NULL);
+  if (stream->seek_fn == NULL)
+    return svn_error_create(SVN_ERR_STREAM_SEEK_NOT_SUPPORTED, NULL, NULL);
+
+  return svn_error_return(readline_fn(stream->baton,
+                                      stringbuf, eol, eof,
+                                      TRUE,
+                                      stream->read_fn,
+                                      stream->mark_fn,
+                                      stream->seek_fn,
+                                      result_pool, scratch_pool));
+}
+
 svn_error_t *svn_stream_copy3(svn_stream_t *from, svn_stream_t *to,
                               svn_cancel_func_t cancel_func,
                               void *cancel_baton,
@@ -536,7 +500,27 @@ seek_handler_empty(void *baton, svn_stream_mark_t
   return SVN_NO_ERROR;
 }
 
+static svn_error_t *
+readline_handler_empty(void *baton,
+                       svn_read_fn_t read_fn,
+                       svn_io_mark_fn_t mark_fn,
+                       svn_io_seek_fn_t seek_fn,
+                       svn_stringbuf_t **stringbuf,
+                       const char **eol,
+                       svn_boolean_t *eof,
+                       apr_pool_t *result_pool,
+                       apr_pool_t *scratch_pool)
+{
+  *stringbuf = svn_stringbuf_create_ensure(0, result_pool);
 
+  if (eol && *eol == NULL)
+    *eol = APR_EOL_STR;
+
+  *eof = TRUE;
+
+  return SVN_NO_ERROR;
+}
+
 svn_stream_t *
 svn_stream_empty(apr_pool_t *pool)
 {
@@ -548,6 +532,8 @@ svn_stream_empty(apr_pool_t *pool)
   svn_stream_set_reset(stream, reset_handler_empty);
   svn_stream_set_mark(stream, mark_handler_empty);
   svn_stream_set_seek(stream, seek_handler_empty);
+  svn_stream_set_readline(stream, readline_handler_empty);
+
   return stream;
 }
 
@@ -652,6 +638,7 @@ svn_stream_disown(svn_stream_t *stream, apr_pool_t
   svn_stream_set_reset(s, reset_handler_disown);
   svn_stream_set_mark(s, mark_handler_disown);
   svn_stream_set_seek(s, seek_handler_disown);
+  svn_stream_set_readline(s, stream_readline);
 
   return s;
 }
@@ -820,6 +807,7 @@ svn_stream_from_aprfile2(apr_file_t *file,
   svn_stream_set_reset(stream, reset_handler_apr);
   svn_stream_set_mark(stream, mark_handler_apr);
   svn_stream_set_seek(stream, seek_handler_apr);
+  svn_stream_set_readline(stream, stream_readline);
 
   if (! disown)
     svn_stream_set_close(stream, close_handler_apr);
@@ -898,6 +886,7 @@ svn_stream_from_aprfile_range_readonly(apr_file_t
   svn_stream_set_reset(stream, reset_handler_apr);
   svn_stream_set_mark(stream, mark_handler_apr);
   svn_stream_set_seek(stream, seek_handler_apr);
+  svn_stream_set_readline(stream, stream_readline);
 
   if (! disown)
     svn_stream_set_close(stream, close_handler_apr);
@@ -1187,6 +1176,7 @@ svn_stream_compressed(svn_stream_t *stream, apr_po
   svn_stream_set_read(zstream, read_handler_gz);
   svn_stream_set_write(zstream, write_handler_gz);
   svn_stream_set_close(zstream, close_handler_gz);
+  svn_stream_set_readline(zstream, stream_readline);
 
   return zstream;
 }
@@ -1302,6 +1292,8 @@ svn_stream_checksummed2(svn_stream_t *stream,
   svn_stream_set_read(s, read_handler_checksum);
   svn_stream_set_write(s, write_handler_checksum);
   svn_stream_set_close(s, close_handler_checksum);
+  svn_stream_set_readline(s, stream_readline);
+
   return s;
 }
 
@@ -1384,6 +1376,8 @@ svn_stream_checksummed(svn_stream_t *stream,
   svn_stream_set_read(s, read_handler_md5);
   svn_stream_set_write(s, write_handler_md5);
   svn_stream_set_close(s, close_handler_md5);
+  svn_stream_set_readline(s, stream_readline);
+
   return s;
 }
 
@@ -1475,6 +1469,8 @@ svn_stream_from_stringbuf(svn_stringbuf_t *str,
   svn_stream_set_reset(stream, reset_handler_stringbuf);
   svn_stream_set_mark(stream, mark_handler_stringbuf);
   svn_stream_set_seek(stream, seek_handler_stringbuf);
+  svn_stream_set_readline(stream, stream_readline);
+
   return stream;
 }
 
@@ -1511,6 +1507,8 @@ svn_stream_from_string(const svn_string_t *str,
   baton->amt_read = 0;
   stream = svn_stream_create(baton, pool);
   svn_stream_set_read(stream, read_handler_string);
+  svn_stream_set_readline(stream, stream_readline);
+
   return stream;
 }
 
Index: subversion/libsvn_client/patch.c
===================================================================
--- subversion/libsvn_client/patch.c	(revision 961349)
+++ subversion/libsvn_client/patch.c	(working copy)
@@ -532,7 +532,7 @@ read_line(patch_target_t *target,
 
   SVN_ERR(svn_stream_readline_detect_eol(target->stream, &line_raw,
                                          &eol_str, &target->eof,
-                                         scratch_pool));
+                                         scratch_pool, scratch_pool));
   if (target->eol_style == svn_subst_eol_style_none)
     target->eol_str = eol_str;
 
@@ -632,7 +632,8 @@ match_hunk(svn_boolean_t *matched, patch_target_t
 
       SVN_ERR(svn_stream_readline_detect_eol(hunk->original_text,
                                              &hunk_line, NULL,
-                                             &hunk_eof, iterpool));
+                                             &hunk_eof,
+                                             iterpool, iterpool));
       /* Contract keywords, if any, before matching. */
       SVN_ERR(svn_subst_translate_cstring2(hunk_line->data,
                                            &hunk_line_translated,
@@ -676,7 +677,8 @@ match_hunk(svn_boolean_t *matched, patch_target_t
       /* If the target has no newline at end-of-file, we get an EOF
        * indication for the target earlier than we do get it for the hunk. */
       SVN_ERR(svn_stream_readline_detect_eol(hunk->original_text, &hunk_line,
-                                             NULL, &hunk_eof, iterpool));
+                                             NULL, &hunk_eof,
+                                             iterpool, iterpool));
       if (hunk_line->len == 0 && hunk_eof)
         *matched = lines_matched;
       else
@@ -928,7 +930,8 @@ reject_hunk(patch_target_t *target, hunk_info_t *h
       svn_pool_clear(iterpool);
 
       SVN_ERR(svn_stream_readline_detect_eol(hi->hunk->diff_text, &hunk_line,
-                                             &eol_str, &eof, iterpool));
+                                             &eol_str, &eof, iterpool,
+                                             iterpool));
       if (! eof)
         {
           if (hunk_line->len >= 1)
@@ -994,7 +997,7 @@ apply_hunk(patch_target_t *target, hunk_info_t *hi
 
       SVN_ERR(svn_stream_readline_detect_eol(hi->hunk->modified_text,
                                              &hunk_line, &eol_str,
-                                             &eof, iterpool));
+                                             &eof, iterpool, iterpool));
       lines_read++;
       if (! eof && lines_read > hi->fuzz &&
           lines_read <= hi->hunk->modified_length - hi->fuzz)
Index: subversion/tests/libsvn_subr/stream-test.c
===================================================================
--- subversion/tests/libsvn_subr/stream-test.c	(revision 961349)
+++ subversion/tests/libsvn_subr/stream-test.c	(working copy)
@@ -307,143 +307,7 @@ test_stream_range(apr_pool_t *pool)
     return SVN_NO_ERROR;
 }
 
-/* An implementation of svn_io_line_filter_cb_t */
 static svn_error_t *
-line_filter(svn_boolean_t *filtered, const char *line, void *baton,
-            apr_pool_t *scratch_pool)
-{
-  *filtered = strchr(line, '!') != NULL;
-  return SVN_NO_ERROR;
-}
-
-static svn_error_t *
-test_stream_line_filter(apr_pool_t *pool)
-{
-  static const char *lines[4] = {"Not filtered.", "Filtered!",
-                                 "Not filtered either.", "End of the lines!"};
-  svn_string_t *string;
-  svn_stream_t *stream;
-  svn_stringbuf_t *line;
-  svn_boolean_t eof;
-
-  string = svn_string_createf(pool, "%s\n%s\n%s\n%s", lines[0], lines[1],
-                              lines[2], lines[3]);
-  stream = svn_stream_from_string(string, pool);
-
-  svn_stream_set_line_filter_callback(stream, line_filter);
-
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, lines[0]);
-  /* line[1] should be filtered */
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, lines[2]);
-
-  /* The last line should also be filtered, and the resulting
-   * stringbuf should be empty. */
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_ASSERT(eof && svn_stringbuf_isempty(line));
-
-  return SVN_NO_ERROR;
-}
-
-/* An implementation of svn_io_line_transformer_cb_t */
-static svn_error_t *
-line_transformer(svn_stringbuf_t **buf, const char *line, void *baton,
-                 apr_pool_t *result_pool, apr_pool_t *scratch_pool)
-{
-  int i, len = strlen(line);
-  char *temp = apr_palloc(scratch_pool, len + 1 );
-
-  for (i = 0; i < len; i++)
-    {
-      temp[i] = line[len - 1 - i];
-    }
-
-  temp[len] = '\0';
-
-  *buf = svn_stringbuf_create(temp, result_pool);
-
-  return SVN_NO_ERROR;
-}
-
-static svn_error_t *
-test_stream_line_transformer(apr_pool_t *pool)
-{
-  static const char *lines[4] = {"gamma", "",
-                                 "iota", "!"};
-
-  static const char *inv_lines[4] = {"ammag", "",
-                                 "atoi", "!"};
-  svn_string_t *string;
-  svn_stream_t *stream;
-  svn_stringbuf_t *line;
-  svn_boolean_t eof;
-
-  string = svn_string_createf(pool, "%s\n%s\n%s\n%s", lines[0], lines[1],
-                              lines[2], lines[3]);
-
-  stream = svn_stream_from_string(string, pool);
-
-  svn_stream_set_line_transformer_callback(stream, line_transformer);
-
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, inv_lines[0]);
-
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, inv_lines[1]);
-
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, inv_lines[2]);
-
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, inv_lines[3]);
-
-  /* We should have reached eof and the stringbuf should be emtpy. */
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_ASSERT(eof && svn_stringbuf_isempty(line));
-
-  return SVN_NO_ERROR;
-}
-
-static svn_error_t *
-test_stream_line_filter_and_transformer(apr_pool_t *pool)
-{
-  static const char *lines[4] = {"!gamma", "",
-                                 "iota", "!"};
-
-  static const char *inv_lines[4] = {"ammag", "",
-                                 "atoi", "!"};
-  svn_string_t *string;
-  svn_stream_t *stream;
-  svn_stringbuf_t *line;
-  svn_boolean_t eof;
-
-  string = svn_string_createf(pool, "%s\n%s\n%s\n%s", lines[0], lines[1],
-                              lines[2], lines[3]);
-
-  stream = svn_stream_from_string(string, pool);
-
-  svn_stream_set_line_filter_callback(stream, line_filter);
-
-  svn_stream_set_line_transformer_callback(stream, line_transformer);
-
-  /* Line one should be filtered. */
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, inv_lines[1]);
-
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, inv_lines[2]);
-
-  /* The last line should also be filtered, and the resulting
-   * stringbuf should be empty. */
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_ASSERT(eof && svn_stringbuf_isempty(line));
-
-  return SVN_NO_ERROR;
-
-}
-
-static svn_error_t *
 test_stream_tee(apr_pool_t *pool)
 {
   svn_stringbuf_t *test_bytes = generate_test_bytes(100, pool);
@@ -647,12 +511,6 @@ struct svn_test_descriptor_t test_funcs[] =
                    "test compressed streams"),
     SVN_TEST_PASS2(test_stream_range,
                    "test streams reading from range of file"),
-    SVN_TEST_PASS2(test_stream_line_filter,
-                   "test stream line filtering"),
-    SVN_TEST_PASS2(test_stream_line_transformer,
-                   "test stream line transforming"),
-    SVN_TEST_PASS2(test_stream_line_filter_and_transformer,
-                   "test stream line filtering and transforming"),
     SVN_TEST_PASS2(test_stream_tee,
                    "test 'tee' streams"),
     SVN_TEST_PASS2(test_stream_seek_file,

Re: [PATCH] issue #3555: make svn_stream_readline() a stream method

Posted by Julian Foad <ju...@wandisco.com>.
On Thu, 2010-07-08 at 15:23 +0200, Stefan Sperling wrote:
> On Thu, Jul 08, 2010 at 02:08:15PM +0200, Stefan Sperling wrote:
> > On Thu, Jul 08, 2010 at 12:39:22PM +0100, Julian Foad wrote:
> > > I've only skimmed through it so far, and probably can't do a full review
> > > before the end of the month.  However from what I've seen it looks good
> > > enough to go ahead and then improve/fix later.
> > 
> > OK, I'll fix up the bits above and commit.
> 
> FYI, on IRC, we decided to go for a different approach:

... because we realized that the semantics weren't really defined, e.g.
what's supposed to happen when you stack one stream with an custom
readline() method on top of another stream with a custom readline()
method, and/or one or both of them transform the lines such that a
series of readline() gives different data from a series of read().  As
readline() is not a primitive stream op, but is defined in terms of
read(), it's not clear how it's supposed to work in general.

Berty suggested, and we agreed, that readline() is a higher level
concept than stream-of-bytes, and so maybe we would want to consider
implementing some "stream of lines" concept that could be built on a
stream-of-bytes but not vice-versa.

> <stsp> julianf, look at svn_hunk_t in svn_diff.h
> <stsp> the streams in there need special handling when reading lines from them
> <stsp> instead of overloading readline we can simply add a set of helper functions that are meant to be used for reading lines from those streams
> <stsp> that is specific to what we need in svn patch,
> <stsp> and avoids having to worry about all the other generic streams
> <stsp> and if a user calls the diff-specific stream reader functions on a generic stream, that's the user's problem
> <julianf> Yes, that sounds better.
> <stsp> ok, I'll do that then

I think anything local to diff/patch like that sounds better than the
status quo of having special readline() behaviour tacked on to the
generic stream class, but I didn't consider what you mean exactly or
what would really be the best way to structure the diff/patch streamy
code.

- Julian


Re: [PATCH] issue #3555: make svn_stream_readline() a stream method

Posted by Stefan Sperling <st...@elego.de>.
On Thu, Jul 08, 2010 at 02:08:15PM +0200, Stefan Sperling wrote:
> On Thu, Jul 08, 2010 at 12:39:22PM +0100, Julian Foad wrote:
> > I've only skimmed through it so far, and probably can't do a full review
> > before the end of the month.  However from what I've seen it looks good
> > enough to go ahead and then improve/fix later.
> 
> OK, I'll fix up the bits above and commit.

FYI, on IRC, we decided to go for a different approach:

<stsp> julianf, look at svn_hunk_t in svn_diff.h
<stsp> the streams in there need special handling when reading lines from them
<stsp> instead of overloading readline we can simply add a set of helper functions that are meant to be used for reading lines from those streams
<stsp> that is specific to what we need in svn patch,
<stsp> and avoids having to worry about all the other generic streams
<stsp> and if a user calls the diff-specific stream reader functions on a generic stream, that's the user's problem
<julianf> Yes, that sounds better.
<stsp> ok, I'll do that then

Re: [PATCH] issue #3555: make svn_stream_readline() a stream method

Posted by Stefan Sperling <st...@elego.de>.
On Thu, Jul 08, 2010 at 12:39:22PM +0100, Julian Foad wrote:
> (Oops, I forgot to trim 60 KB of quoted text in my previous reply :-( )
> 
> 
> Would you mind writing a test.  The thing that most needs testing, I
> should think, is interleaving read(), readline(), mark() and seek().
> Here's an idea for a test:

That's a good idea. I'll do that in a separate commit.

> Can you see a convenient way to keep the transformer/filter tests?  It
> seems a bit of a pity to delete them.

It is a pity, but I might as well write a new test like you proposed.

> > +/* Invoke the READ_FN function of a stream to scan  for an end-of-line
> > + * indicatior, and return it in *EOL.
> 
> s/indicatior/indicator

Fixed.

> > + * Use MARK_FN and SEEK_FN to seek in the stream.
> 
> Why should it seek in the stream?  Ah, because it returns to the
> original position.  Let's say so:
> 
>   Set *EOL to the first end-of-line string found in the stream
>   accessed through READ_FN, MARK_FN and SEEK_FN, whose stream baton
>   is BATON.  Leave the stream read position unchanged.  Allocate
>   *EOL statically; POOL is a scratch pool.
> 
> That also documents the other parameters.

Thanks, I've used this docstring.

> >   * Set *EOL to NULL if the stream runs out before an end-of-line indicator
> >   * is found. */
> >  static svn_error_t *
> > -scan_eol(const char **eol, svn_stream_t *stream, apr_pool_t *pool)
> > +scan_eol(void *baton,
> > +         svn_read_fn_t read_fn,
> > +         svn_io_mark_fn_t mark_fn,
> > +         svn_io_seek_fn_t seek_fn,
> > +         const char **eol,
> > +         apr_pool_t *pool)
> >  {
> 
> 
> Would it make more sense for stream_create() to initialize the readline
> member to point to the default stream_readline() function, instead of
> writing "if (readline == NULL) X else Y" in two places?  That would also
> allow a custom stream impl to set the readline handler to NULL if
> readline doesn't make sense, e.g. on a compress-while-reading stream.
> Not sure if that's better, just asking.

Sure. We can add an error code and make svn_stream_readline() return
an error if readline_fn is NULL. Just like it's done for reset/mark/seek.
 
> > @@ -1225,11 +1196,14 [...]
> >  /**
> > - * Similar to svn_stream_readline(). The line-terminator is detected
> > - * automatically.  If @a eol is not NULL, the detected line-terminator
> > - * is returned in @a *eol.  If EOF is reached and the stream does not
> > - * end with a newline character, @a *eol will be NULL.
> > + * Similar to svn_stream_readline(). If @a *eol is not NULL, it is used
> > + * as the expected line terminator. If @a eol is NULL, the line-terminator
> > + * is detected automatically. If @a *eol is NULL, the line-terminator is
> > + * detected automatically and is returned in @a *eol.
> > [...]
> >   */
> >  svn_error_t *
> >  svn_stream_readline_detect_eol([...]
> 
> Did you mean to change the API here to allow specifying the expected EOL
> string?  That seems an unnecessary change, and your impl. doesn't
> support that on non-seekable streams, so it looks unintended:

See my other mail, this change has been reverted.

> > @@ -416,13 +359,35 @@ svn_stream_readline_detect_eol([...]
> > [...]
> > +  /* EOL-detection requires mark/seek support. */
> > +  if (stream->mark_fn == NULL)
> > +    return svn_error_create(SVN_ERR_STREAM_SEEK_NOT_SUPPORTED, NULL, NULL);
> > +  if (stream->seek_fn == NULL)
> > +    return svn_error_create(SVN_ERR_STREAM_SEEK_NOT_SUPPORTED, NULL, NULL);
> 
> 
> The empty stream impl looks wrong: who says readline_handler_empty()
> should return APR_EOL_STR as the "detected" EOL string?  If that's
> proper behaviour for svn_io_readline_fn_t, it needs to be documented in
> svn_io_readline_fn_t.

Actually, it should set it to NULL to conform to the API. Fixed.
I also forgot to update its parameter list in the last diff I sent :-/

> The forwarding in the several filter streams looks wrong:
> 
> > @@ -652,6 +639,7 @@ svn_stream_disown(svn_stream_t *stream, apr_pool_t
> >    svn_stream_set_reset(s, reset_handler_disown);
> >    svn_stream_set_mark(s, mark_handler_disown);
> >    svn_stream_set_seek(s, seek_handler_disown);
> > +  svn_stream_set_readline(s, stream_readline);
> 
> It should forward to the wrapped stream's readline function, not the
> default readline, shouldn't it?  (Several similar places.)

Yes, you're right. Taking that shortcut is wrong because it can
inadvertently override a custom stream method.

> I've only skimmed through it so far, and probably can't do a full review
> before the end of the month.  However from what I've seen it looks good
> enough to go ahead and then improve/fix later.

OK, I'll fix up the bits above and commit.
Thanks for review :)

Stefan

Re: [PATCH] issue #3555: make svn_stream_readline() a stream method

Posted by Julian Foad <ju...@wandisco.com>.
(Oops, I forgot to trim 60 KB of quoted text in my previous reply :-( )


Would you mind writing a test.  The thing that most needs testing, I
should think, is interleaving read(), readline(), mark() and seek().
Here's an idea for a test:

  Stream content = "Line one.\nLine two.\n"

  # Some points where we will mark and seek:
[[[<1>Line one.
<2>Line <3>two.<4>
<5>]]]

  # Sequentially read, mixing read() with readline(), and make marks.
  mark(<1>)  # before any read or readline
  readline() == "Line one."
  mark(<2>)  # after readline
  read(5 bytes) == "Line "
  mark(<3>)  # after read
  read(4 bytes) == "two."
  mark(<4>)  # after read at EOL
  readline() == ""
  mark(<5>)  # after read after EOL
  readline() == EOF

  # Jump around with seeks, and check read() and readline().
  seek(<1>)
  readline() == "Line one."
  seek(<5>)
  readline() == EOF
  seek(<2>)
  read(5 bytes) == "Line "
  seek(<4>)
  readline() == ""
  seek(<3>)
  readline() == "two."

  # And then a test with one stream wrapped in another would be ...
  # useful, though harder to write.


Can you see a convenient way to keep the transformer/filter tests?  It
seems a bit of a pity to delete them.


> +/* Invoke the READ_FN function of a stream to scan  for an end-of-line
> + * indicatior, and return it in *EOL.

s/indicatior/indicator

> + * Use MARK_FN and SEEK_FN to seek in the stream.

Why should it seek in the stream?  Ah, because it returns to the
original position.  Let's say so:

  Set *EOL to the first end-of-line string found in the stream
  accessed through READ_FN, MARK_FN and SEEK_FN, whose stream baton
  is BATON.  Leave the stream read position unchanged.  Allocate
  *EOL statically; POOL is a scratch pool.

That also documents the other parameters.

>   * Set *EOL to NULL if the stream runs out before an end-of-line indicator
>   * is found. */
>  static svn_error_t *
> -scan_eol(const char **eol, svn_stream_t *stream, apr_pool_t *pool)
> +scan_eol(void *baton,
> +         svn_read_fn_t read_fn,
> +         svn_io_mark_fn_t mark_fn,
> +         svn_io_seek_fn_t seek_fn,
> +         const char **eol,
> +         apr_pool_t *pool)
>  {


Would it make more sense for stream_create() to initialize the readline
member to point to the default stream_readline() function, instead of
writing "if (readline == NULL) X else Y" in two places?  That would also
allow a custom stream impl to set the readline handler to NULL if
readline doesn't make sense, e.g. on a compress-while-reading stream.
Not sure if that's better, just asking.


> @@ -1225,11 +1196,14 [...]
>  /**
> - * Similar to svn_stream_readline(). The line-terminator is detected
> - * automatically.  If @a eol is not NULL, the detected line-terminator
> - * is returned in @a *eol.  If EOF is reached and the stream does not
> - * end with a newline character, @a *eol will be NULL.
> + * Similar to svn_stream_readline(). If @a *eol is not NULL, it is used
> + * as the expected line terminator. If @a eol is NULL, the line-terminator
> + * is detected automatically. If @a *eol is NULL, the line-terminator is
> + * detected automatically and is returned in @a *eol.
> [...]
>   */
>  svn_error_t *
>  svn_stream_readline_detect_eol([...]

Did you mean to change the API here to allow specifying the expected EOL
string?  That seems an unnecessary change, and your impl. doesn't
support that on non-seekable streams, so it looks unintended:

> @@ -416,13 +359,35 @@ svn_stream_readline_detect_eol([...]
> [...]
> +  /* EOL-detection requires mark/seek support. */
> +  if (stream->mark_fn == NULL)
> +    return svn_error_create(SVN_ERR_STREAM_SEEK_NOT_SUPPORTED, NULL, NULL);
> +  if (stream->seek_fn == NULL)
> +    return svn_error_create(SVN_ERR_STREAM_SEEK_NOT_SUPPORTED, NULL, NULL);


The empty stream impl looks wrong: who says readline_handler_empty()
should return APR_EOL_STR as the "detected" EOL string?  If that's
proper behaviour for svn_io_readline_fn_t, it needs to be documented in
svn_io_readline_fn_t.


The forwarding in the several filter streams looks wrong:

> @@ -652,6 +639,7 @@ svn_stream_disown(svn_stream_t *stream, apr_pool_t
>    svn_stream_set_reset(s, reset_handler_disown);
>    svn_stream_set_mark(s, mark_handler_disown);
>    svn_stream_set_seek(s, seek_handler_disown);
> +  svn_stream_set_readline(s, stream_readline);

It should forward to the wrapped stream's readline function, not the
default readline, shouldn't it?  (Several similar places.)


I've only skimmed through it so far, and probably can't do a full review
before the end of the month.  However from what I've seen it looks good
enough to go ahead and then improve/fix later.

- Julian



I (Julian Foad) wrote:
> Hi Stefan.  I've just started looking at this.  Overall impression is it
> looks like you've done it very well - thank you!
> 
> Could you possibly get the readline_detect_eol() API changes out of this
> patch, and make them in a separate patch either before or after this?
> Or if that change belongs in this patch, please explain why in the log
> msg.
> 
> Thanks.
> - Julian
> 
> 
> On Wed, 2010-07-07 at 20:39 +0200, Stefan Sperling wrote:
[...]


Re: [PATCH] issue #3555: make svn_stream_readline() a stream method

Posted by Stefan Sperling <st...@elego.de>.
On Thu, Jul 08, 2010 at 09:52:51AM +0100, Julian Foad wrote:
> Hi Stefan.  I've just started looking at this.  Overall impression is it
> looks like you've done it very well - thank you!
> 
> Could you possibly get the readline_detect_eol() API changes out of this
> patch, and make them in a separate patch either before or after this?
> Or if that change belongs in this patch, please explain why in the log
> msg.

It's actually not needed. I think I added it trying to make the
parameter list of svn_io_readline_fn_t match the ones of
svn_stream_readline and svn_stream_readline_detect_eol as closely
as possible.

Looking at it again, adding another parameter to svn_io_readline_fn_t
which says whether the EOL should be detected is much cleaner.
This simplifies implementations, and allows callers of
svn_stream_readline_detect_eol to be lazy about initialising the
eol output parameter.

Updated diff (verified by running just the patch tests which exercise
stream_readline a lot) and updated log message below. Thanks!

[[[
Fix issue #3555, 'Remove the "line_filter" and "line_transformer" callbacks
from svn_stream_t'.

Make svn_stream_readline() a virtual method of svn_stream_t,
and provide custom implementations of readline methods in the diff
parsing code (which is the only place where we need this right now).

* subversion/include/svn_io.h
  (svn_io_line_filter_cb_t, svn_io_line_transformer_cb_t,
   svn_stream_set_line_filter_callback,
   svn_stream_set_line_transformer_callback): Remove.
  (svn_io_readline_fn_t, svn_stream_set_readline): Declare.
  (svn_stream_readline): Adjust docstring.
  (svn_stream_readline_detect_eol): Switch to dual-pool paradigm and
   document that the stream needs to support mark and seek.

* subversion/libsvn_diff/parse-diff.c
  (original_line_filter, modified_line_filter, remove_leading_char_transformer,
   reverse_diff_transformer): Remove.
  (hunk_text_stream_baton, read_handler_hunk_text, write_handler_hunk_text,
   close_handler_hunk_text, reset_handler_hunk_text, mark_handler_hunk_text,
   seek_handler_hunk_text, scan_eol, readline_handler_hunk_text,
   stream_hunk_text, stream_hunk_text_original, stream_hunk_text_modified):
    New. Implementation of a stream which provides a special readline
    method to read original/modified texts of hunks from a patch file stream.
  (reverse_diff_text_stream_baton, read_handler_reverse_diff_text,
   write_handler_reverse_diff_text, close_handler_reverse_diff_text,
   reset_handler_reverse_diff_text, mark_handler_reverse_diff_text,
   seek_handler_reverse_diff_text, readline_handler_reverse_diff_text,
   stream_reverse_diff_text): New. Implementation of a stream which
    provides a special readline method which reverses unidiff data read
    from the wrapped stream.
  (parse_next_hunk): Track svn_stream_readline_detect_eol() dual-pool change.
   Add a comment explaining why the patch file gets opened multiple
   times (drive-by fix because this confused me at first).
   Instead of installing line-filter/transformation callbacks on
   streams, wrap streams with appropriate wrapper streams.

* subversion/libsvn_subr/stream.c
  (struct svn_stream_t): Replace line_filter_cb and line_transformer_cb
   members with readline_fn member.
  (svn_stream_create): Track changes to svn_stream_t.
  (svn_stream_set_line_filter_callback,
   svn_stream_set_line_transformer_callback,
   line_filter, line_transformer): Remove.
  (scan_eol): Tweak argument list for use within a stream method.
   This function can no longer expect a stream, so pass a baton
   and a set of required stream methods instead.
  (stream_readline): Make this an svn_io_readline_fn_t implementation.
   Remove handling of line filters/transformers.
  (svn_stream_readline): Instead of calling the stream_readline() helper
   directly, call a custom readline implementation if one is set on the
   stream. If no custom implementation is provided, fall back to the
   stream_readline() helper function to preserve compatibility with 1.6.x.
  (svn_stream_readline_detect_eol): As previous, and ensure that
   the stream has mark/seek support as it is needed for EOL detection.
  (readline_handler_empty): Custom readline handler for the empty stream.
  (svn_stream_empty, svn_stream_disown, svn_stream_from_aprfile2,
   svn_stream_from_aprfile_range_readonly, svn_stream_compressed,
   svn_stream_checksummed2, svn_stream_checksummed, svn_stream_from_stringbuf,
   svn_stream_from_string): Set a readline method.

* subversion/libsvn_client/patch.c
  (read_line, match_hunk, reject_hunk, apply_hunk): Pass two pools to
   svn_stream_readline_detect_eol().

* subversion/tests/libsvn_subr/stream-test.c
  (line_filter, test_stream_line_filter, line_transformer,
   test_stream_line_transformer,
   test_stream_line_filter_and_transformer): Remove these tests.
  (test_funcs): Remove removed tests.
]]]


Index: subversion/include/svn_io.h
===================================================================
--- subversion/include/svn_io.h	(revision 961349)
+++ subversion/include/svn_io.h	(working copy)
@@ -780,40 +780,39 @@ typedef svn_error_t *(*svn_io_mark_fn_t)(void *bat
 typedef svn_error_t *(*svn_io_seek_fn_t)(void *baton,
                                          svn_stream_mark_t *mark);
 
-/** Line-filtering callback function for a generic stream.
- * @a baton is the stream's baton.
- * @see svn_stream_t, svn_stream_set_baton() and svn_stream_readline().
+/** Line-reading handler function for a generic stream.
  *
- * @since New in 1.7.
- */
-typedef svn_error_t *(*svn_io_line_filter_cb_t)(svn_boolean_t *filtered,
-                                                const char *line,
-                                                void *baton,
-                                                apr_pool_t *scratch_pool);
-
-/** A callback function, invoked by svn_stream_readline(), which can perform
- * arbitary transformations on the line before it is passed back to the caller
- * of svn_stream_readline().
+ * Allocate @a *stringbuf in @a result_pool, and read into it one line
+ * from the stream. @a baton is the stream's baton.
+ * If @a detect_eol is @c FALSE, @a *eol is used as the expected line
+ * terminator. If @a detect_eol is @c TRUE, the line-terminator is
+ * detected automatically and stored in @a *eol if @a eol is not NULL.
+ * If EOF is reached and the stream does not end with a newline character,
+ * and @a eol is not NULL, @a *eol is set to NULL.
  *
- * Returns a transformed stringbuf in @a buf, allocated in @a result_pool.
- * This callback gets invoked on lines which were not filtered by the
- * line-filtering callback function.
+ * @a mark_fn and @a seek_fn are required to be non-NULL if the end-of-line
+ * indicator is to be detected automatically. Else, they may be NULL.
  *
- * Implementations should always at least return an empty stringbuf.
- * It is a fatal error if an implementation returns @a *buf as NULL.
+ * The line-terminator is read from the stream, but is not added to
+ * the end of the stringbuf.  Instead, the stringbuf ends with a usual '\\0'.
  *
- * @a baton is the stream's baton.
+ * If @a stream runs out of bytes before encountering a line-terminator,
+ * then set @a *eof to @c TRUE, otherwise set @a *eof to @c FALSE.
  *
- * @see svn_stream_t, svn_stream_set_baton(), svn_io_line_filter_cb_t and
- * svn_stream_readline().
+ * Temporary allocations will be performed in @a scratch_pool.
  *
- * @since New in 1.7.
- */
-typedef svn_error_t *(*svn_io_line_transformer_cb_t)(svn_stringbuf_t **buf,
-                                                     const char *line,
-                                                     void *baton,
-                                                     apr_pool_t *result_pool,
-                                                     apr_pool_t *scratch_pool);
+ * @see svn_stream_readline and svn_stream_readline_detect_eol.
+ * @since New in 1.7. */
+typedef svn_error_t *(*svn_io_readline_fn_t)(void *baton,
+                                             svn_stringbuf_t **stringbuf,
+                                             const char **eol,
+                                             svn_boolean_t *eof,
+                                             svn_boolean_t detect_eol,
+                                             svn_read_fn_t read_fn,
+                                             svn_io_mark_fn_t mark_fn,
+                                             svn_io_seek_fn_t seek_fn,
+                                             apr_pool_t *result_pool,
+                                             apr_pool_t *scratch_pool);
 
 /** Create a generic stream.  @see svn_stream_t. */
 svn_stream_t *
@@ -864,23 +863,11 @@ void
 svn_stream_set_seek(svn_stream_t *stream,
                     svn_io_seek_fn_t seek_fn);
 
-/** Set @a stream's line-filtering callback function to @a line_filter_cb
- *
- * @since New in 1.7.
- */
+/** Set @a stream's readline function to @a readline_fn */
 void
-svn_stream_set_line_filter_callback(svn_stream_t *stream,
-                                    svn_io_line_filter_cb_t line_filter_cb);
+svn_stream_set_readline(svn_stream_t *stream,
+                        svn_io_readline_fn_t readline_fn);
 
-/** Set @a streams's line-transforming callback function to
- * @a line_transformer_cb.
- *
- * @since New in 1.7.
- */
-void
-svn_stream_set_line_transformer_callback(
-  svn_stream_t *stream,
-  svn_io_line_transformer_cb_t line_transformer_cb);
 
 /** Create a stream that is empty for reading and infinite for writing. */
 svn_stream_t *
@@ -1203,19 +1190,6 @@ svn_stream_printf_from_utf8(svn_stream_t *stream,
  *
  * If @a stream runs out of bytes before encountering a line-terminator,
  * then set @a *eof to @c TRUE, otherwise set @a *eof to FALSE.
- *
- * If a line-filter callback function was set on the stream using
- * svn_stream_set_line_filter_callback(), lines will only be returned
- * if they pass the filtering decision of the callback function.
- * If end-of-file is encountered while reading the line and the line
- * is filtered, @a *stringbuf will be empty.
- *
- * If a line-transformer callback function was set on the stream using
- * svn_stream_set_line_transformer_callback(), lines will be returned
- * transformed, in a way determined by the callback.
- *
- * Note that the line-transformer callback gets called after the line-filter
- * callback, not before.
  */
 svn_error_t *
 svn_stream_readline(svn_stream_t *stream,
@@ -1230,6 +1204,11 @@ svn_stream_readline(svn_stream_t *stream,
  * is returned in @a *eol.  If EOF is reached and the stream does not
  * end with a newline character, @a *eol will be NULL.
  *
+ * The @a stream is required to support mark and seek. @see svn_stream_mark.
+ *
+ * The @a *stringbuf will be allocated in @a result_pool.
+ * @a scratch_pool is used for temporary allocations.
+ *
  * @since New in 1.7.
  */
 svn_error_t *
@@ -1237,7 +1216,8 @@ svn_stream_readline_detect_eol(svn_stream_t *strea
                                svn_stringbuf_t **stringbuf,
                                const char **eol,
                                svn_boolean_t *eof,
-                               apr_pool_t *pool);
+                               apr_pool_t *result_pool,
+                               apr_pool_t *scratch_pool);
 
 /**
  * Read the contents of the readable stream @a from and write them to the
Index: subversion/libsvn_diff/parse-diff.c
===================================================================
--- subversion/libsvn_diff/parse-diff.c	(revision 961349)
+++ subversion/libsvn_diff/parse-diff.c	(working copy)
@@ -33,6 +33,8 @@
 #include "svn_dirent_uri.h"
 #include "svn_diff.h"
 
+#include "private/svn_eol_private.h"
+
 /* Helper macro for readability */
 #define starts_with(str, start)  \
   (strncmp((str), (start), strlen(start)) == 0)
@@ -184,89 +186,405 @@ parse_hunk_header(const char *header, svn_hunk_t *
   return TRUE;
 }
 
-/* A stream line-filter which allows only original text from a hunk,
- * and filters special lines (which start with a backslash). */
+/* Users of the diff parsing API are provided with various streams
+ * to read lines from a hunk. These streams return:
+ *
+ *  - the original hunk text (all lines starting with '-' or ' ')
+ *  - the modified hunk text (all lines starting with '+' or ' ')
+ *  - the plain unidiff text (possibly reversed)
+ *
+ * To achieve this, we wrap the patch file stream with custom streams,
+ * which override the wrapped stream's readline method. */
+
+/* Baton for a stream that reads hunk texts. */
+struct hunk_text_stream_baton {
+  /* The leading unidiff symbol of lines which should be filtered.
+   * Can be '+' or '-', depending on whether we're providing the original
+   * or modified version of the hunk. */
+  char verboten;
+
+  svn_stream_t *wrapped_stream;
+};
+
+/* An implementation of svn_read_fn_t. */
 static svn_error_t *
-original_line_filter(svn_boolean_t *filtered, const char *line, void *baton,
-                     apr_pool_t *scratch_pool)
+read_handler_hunk_text(void *baton, char *buffer, apr_size_t *len)
 {
-  *filtered = (line[0] == '+' || line[0] == '\\');
-  return SVN_NO_ERROR;
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_read(b->wrapped_stream, buffer, len);
 }
 
-/* A stream line-filter which allows only modified text from a hunk,
- * and filters special lines (which start with a backslash). */
+/* An implementation of svn_write_fn_t. */
 static svn_error_t *
-modified_line_filter(svn_boolean_t *filtered, const char *line, void *baton,
-                     apr_pool_t *scratch_pool)
+write_handler_hunk_text(void *baton, const char *buffer, apr_size_t *len)
 {
-  *filtered = (line[0] == '-' || line[0] == '\\');
-  return SVN_NO_ERROR;
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_write(b->wrapped_stream, buffer, len);
 }
 
-/** line-transformer callback to shave leading diff symbols. */
+/* An implementation of svn_close_fn_t. */
 static svn_error_t *
-remove_leading_char_transformer(svn_stringbuf_t **buf,
-                                const char *line,
-                                void *baton,
-                                apr_pool_t *result_pool,
-                                apr_pool_t *scratch_pool)
+close_handler_hunk_text(void *baton)
 {
-  if (line[0] == '+' || line[0] == '-' || line[0] == ' ')
-    *buf = svn_stringbuf_create(line + 1, result_pool);
-  else
-    *buf = svn_stringbuf_create(line, result_pool);
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_close(b->wrapped_stream);
+}
 
+/* An implementation of svn_io_reset_fn_t. */
+static svn_error_t *
+reset_handler_hunk_text(void *baton)
+{
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_reset(b->wrapped_stream);
+}
+
+/* An implementation of svn_io_mark_fn_t. */
+static svn_error_t *
+mark_handler_hunk_text(void *baton, svn_stream_mark_t **mark, apr_pool_t *pool)
+{
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_mark(b->wrapped_stream, mark, pool);
+}
+
+/* An implementation of svn_io_seek_fn_t. */
+static svn_error_t *
+seek_handler_hunk_text(void *baton, svn_stream_mark_t *mark)
+{
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_seek(b->wrapped_stream, mark);
+}
+
+/* Invoke the READ_FN function of a stream to scan for an end-of-line
+ * indicator, and return it in *EOL.
+ * Set *EOL to NULL if the stream runs out before an end-of-line indicator
+ * is found. */
+static svn_error_t *
+scan_eol(void *baton,
+         svn_read_fn_t read_fn,
+         svn_io_mark_fn_t mark_fn,
+         svn_io_seek_fn_t seek_fn,
+         const char **eol,
+         apr_pool_t *pool)
+{
+  const char *eol_str;
+  svn_stream_mark_t *mark;
+
+  SVN_ERR(mark_fn(baton, &mark, pool));
+
+  eol_str = NULL;
+  while (! eol_str)
+    {
+      char buf[512];
+      apr_size_t len;
+
+      len = sizeof(buf);
+      SVN_ERR(read_fn(baton, buf, &len));
+      if (len == 0)
+          break; /* EOF */
+      eol_str = svn_eol__detect_eol(buf, buf + len);
+    }
+
+  SVN_ERR(seek_fn(baton, mark));
+
+  *eol = eol_str;
+
   return SVN_NO_ERROR;
 }
 
-/** line-transformer callback to reverse a diff text. */
+/* An implementation of svn_io_readline_fn_t. */
 static svn_error_t *
-reverse_diff_transformer(svn_stringbuf_t **buf,
-                         const char *line,
-                         void *baton,
-                         apr_pool_t *result_pool,
-                         apr_pool_t *scratch_pool)
+readline_handler_hunk_text(void *baton,
+                           svn_stringbuf_t **stringbuf,
+                           const char **eol,
+                           svn_boolean_t *eof,
+                           svn_boolean_t detect_eol,
+                           svn_read_fn_t read_fn,
+                           svn_io_mark_fn_t mark_fn,
+                           svn_io_seek_fn_t seek_fn,
+                           apr_pool_t *result_pool,
+                           apr_pool_t *scratch_pool)
 {
-  svn_hunk_t hunk;
+  svn_stringbuf_t *str;
+  apr_pool_t *iterpool;
+  svn_boolean_t filtered;
+  const char *eol_str;
+  struct hunk_text_stream_baton *b = baton;
 
-  /* ### Pass the already parsed hunk via the baton?
-   * ### Maybe we should really make svn_stream_readline() a proper stream
-   * ### method and override it instead of adding special callbacks? */
-  if (parse_hunk_header(line, &hunk, "@@", FALSE, scratch_pool))
+  *eof = FALSE;
+
+  iterpool = svn_pool_create(scratch_pool);
+  do
     {
-      *buf = svn_stringbuf_createf(result_pool,
-                                   "@@ -%lu,%lu +%lu,%lu @@",
-                                   hunk.modified_start,
-                                   hunk.modified_length,
-                                   hunk.original_start,
-                                   hunk.original_length);
+      apr_size_t numbytes;
+      const char *match;
+      char c;
+
+      svn_pool_clear(iterpool);
+
+      /* Since we're reading one character at a time, let's at least
+         optimize for the 90% case.  90% of the time, we can avoid the
+         stringbuf ever having to realloc() itself if we start it out at
+         80 chars.  */
+      str = svn_stringbuf_create_ensure(80, iterpool);
+
+      if (detect_eol)
+        {
+          SVN_ERR(scan_eol(baton, read_fn, mark_fn, seek_fn,
+                           &eol_str, iterpool));
+          if (eol)
+            *eol = eol_str;
+          if (eol_str == NULL)
+            {
+              /* No newline until EOF, EOL_STR can be anything. */
+              eol_str = APR_EOL_STR;
+            }
+        }
+      else
+        eol_str = *eol;
+
+      /* Read into STR up to and including the next EOL sequence. */
+      match = eol_str;
+      numbytes = 1;
+      while (*match)
+        {
+          SVN_ERR(read_fn(baton, &c, &numbytes));
+          if (numbytes != 1)
+            {
+              /* a 'short' read means the stream has run out. */
+              *eof = TRUE;
+              /* We know we don't have a whole EOL sequence, but ensure we
+               * don't chop off any partial EOL sequence that we may have. */
+              match = eol_str;
+              /* Process this short (or empty) line just like any other
+               * except with *EOF set. */
+              break;
+            }
+
+          if (c == *match)
+            match++;
+          else
+            match = eol_str;
+
+          svn_stringbuf_appendbytes(str, &c, 1);
+        }
+
+      svn_stringbuf_chop(str, match - eol_str);
+      filtered = (str->data[0] == b->verboten || str->data[0] == '\\');
     }
-  else if (parse_hunk_header(line, &hunk, "##", FALSE, scratch_pool))
+  while (filtered && ! *eof);
+  /* Not destroying the iterpool just yet since we still need STR
+   * which is allocated in it. */
+
+  if (filtered)
     {
-      *buf = svn_stringbuf_createf(result_pool,
-                                   "## -%lu,%lu +%lu,%lu ##",
-                                   hunk.modified_start,
-                                   hunk.modified_length,
-                                   hunk.original_start,
-                                   hunk.original_length);
+      /* EOF, return an empty string. */
+      *stringbuf = svn_stringbuf_create_ensure(0, result_pool);
     }
-  else if (line[0] == '+')
+  else if (str->data[0] == '+' || str->data[0] == '-' || str->data[0] == ' ')
     {
-      *buf = svn_stringbuf_create(line, result_pool);
-      (*buf)->data[0] = '-';
+      /* Shave off leading unidiff symbols. */
+      *stringbuf = svn_stringbuf_create(str->data + 1, result_pool);
     }
-  else if (line[0] == '-')
+  else
     {
-      *buf = svn_stringbuf_create(line, result_pool);
-      (*buf)->data[0] = '+';
+      /* Return the line as-is. */
+      *stringbuf = svn_stringbuf_dup(str, result_pool);
     }
+
+  /* Done. RIP iterpool. */
+  svn_pool_destroy(iterpool);
+
+  return SVN_NO_ERROR;
+}
+
+/* Return a stream for reading hunk text from WRAPPED_STREAM.
+ * VERBOTEN is the leading character of lines which should be
+ * filtered by this stream's readline method ('+' or '-').
+ * Allocate the new stream in RESULT_POOL. */
+static svn_stream_t *
+stream_hunk_text(svn_stream_t *wrapped_stream,
+                 char verboten,
+                 apr_pool_t *result_pool)
+{
+  svn_stream_t *stream;
+  struct hunk_text_stream_baton *baton;
+
+  baton = apr_palloc(result_pool, sizeof(*baton));
+  baton->wrapped_stream = wrapped_stream;
+  baton->verboten = verboten;
+
+  stream = svn_stream_create(baton, result_pool);
+
+  svn_stream_set_read(stream, read_handler_hunk_text);
+  svn_stream_set_write(stream, write_handler_hunk_text);
+  svn_stream_set_close(stream, close_handler_hunk_text);
+  svn_stream_set_reset(stream, reset_handler_hunk_text);
+  svn_stream_set_mark(stream, mark_handler_hunk_text);
+  svn_stream_set_seek(stream, seek_handler_hunk_text);
+  svn_stream_set_readline(stream, readline_handler_hunk_text);
+
+  return stream;
+}
+
+/* Return a stream to read the original text of a hunk from
+ * WRAPPED_STREAM, and allocated in RESULT_POOL.*/
+static svn_stream_t *
+stream_hunk_text_original(svn_stream_t *wrapped_stream,
+                          apr_pool_t *result_pool)
+{
+  return stream_hunk_text(wrapped_stream, '+', result_pool);
+}
+
+/* Return a stream to read the modified text of a hunk from
+ * WRAPPED_STREAM, and allocated in RESULT_POOL.*/
+static svn_stream_t *
+stream_hunk_text_modified(svn_stream_t *wrapped_stream,
+                          apr_pool_t *result_pool)
+{
+  return stream_hunk_text(wrapped_stream, '-', result_pool);
+}
+
+
+/* Baton for a stream that reads unidiff text reversed. */
+struct reverse_diff_text_stream_baton {
+  svn_stream_t *wrapped_stream;
+}; 
+
+/* An implementation of svn_read_fn_t. */
+static svn_error_t *
+read_handler_reverse_diff_text(void *baton, char *buffer, apr_size_t *len)
+{
+  struct reverse_diff_text_stream_baton *b = baton;
+  return svn_stream_read(b->wrapped_stream, buffer, len);
+}
+
+/* An implementation of svn_write_fn_t. */
+static svn_error_t *
+write_handler_reverse_diff_text(void *baton, const char *buffer,
+                                apr_size_t *len)
+{
+  struct reverse_diff_text_stream_baton *b = baton;
+  return svn_stream_write(b->wrapped_stream, buffer, len);
+}
+
+/* An implementation of svn_close_fn_t. */
+static svn_error_t *
+close_handler_reverse_diff_text(void *baton)
+{
+  struct reverse_diff_text_stream_baton *b = baton;
+  return svn_stream_close(b->wrapped_stream);
+}
+
+/* An implementation of svn_io_reset_fn_t. */
+static svn_error_t *
+reset_handler_reverse_diff_text(void *baton)
+{
+  struct reverse_diff_text_stream_baton *b = baton;
+  return svn_stream_reset(b->wrapped_stream);
+}
+
+/* An implementation of svn_io_mark_fn_t. */
+static svn_error_t *
+mark_handler_reverse_diff_text(void *baton, svn_stream_mark_t **mark,
+                               apr_pool_t *pool)
+{
+  struct reverse_diff_text_stream_baton *b = baton;
+  return svn_stream_mark(b->wrapped_stream, mark, pool);
+}
+
+/* An implementation of svn_io_seek_fn_t. */
+static svn_error_t *
+seek_handler_reverse_diff_text(void *baton, svn_stream_mark_t *mark)
+{
+  struct hunk_text_stream_baton *b = baton;
+  return svn_stream_seek(b->wrapped_stream, mark);
+}
+
+/* An implementation of svn_io_readline_fn_t. */
+static svn_error_t *
+readline_handler_reverse_diff_text(void *baton,
+                                   svn_stringbuf_t **stringbuf,
+                                   const char **eol,
+                                   svn_boolean_t *eof,
+                                   svn_boolean_t detect_eol,
+                                   svn_read_fn_t read_fn,
+                                   svn_io_mark_fn_t mark_fn,
+                                   svn_io_seek_fn_t seek_fn,
+                                   apr_pool_t *result_pool,
+                                   apr_pool_t *scratch_pool)
+{
+  svn_hunk_t hunk;
+  svn_stringbuf_t *line;
+  struct reverse_diff_text_stream_baton *b = baton;
+
+  /* Read the line and perform necessary transformations to
+   * produce a reversed diff. */
+  if (detect_eol)
+    SVN_ERR(svn_stream_readline_detect_eol(b->wrapped_stream,
+                                           &line, eol, eof,
+                                           result_pool, scratch_pool));
   else
-    *buf = svn_stringbuf_create(line, result_pool);
+    SVN_ERR(svn_stream_readline(b->wrapped_stream, &line, *eol, eof,
+                                result_pool));
 
+  if (parse_hunk_header(line->data, &hunk, "@@", FALSE, scratch_pool))
+    {
+      /* Line is a hunk header, reverse it. */
+      *stringbuf = svn_stringbuf_createf(result_pool,
+                                         "@@ -%lu,%lu +%lu,%lu @@",
+                                         hunk.modified_start,
+                                         hunk.modified_length,
+                                         hunk.original_start,
+                                         hunk.original_length);
+    }
+  else if (parse_hunk_header(line->data, &hunk, "##", FALSE, scratch_pool))
+    {
+      /* Line is a hunk header, reverse it. */
+      *stringbuf = svn_stringbuf_createf(result_pool,
+                                         "## -%lu,%lu +%lu,%lu ##",
+                                         hunk.modified_start,
+                                         hunk.modified_length,
+                                         hunk.original_start,
+                                         hunk.original_length);
+    }
+  else
+    {
+      if (line->data[0] == '+')
+        line->data[0] = '-';
+      else if (line->data[0] == '-')
+        line->data[0] = '+';
+
+      *stringbuf = line;
+    }
+
   return SVN_NO_ERROR;
 }
 
+/* Return a stream for reading diff text from WRAPPED_STREAM.
+ * The unidiff will appear reversed when read via the stream's readline method.
+ * Allocate the new stream in RESULT_POOL. */
+static svn_stream_t *
+stream_reverse_diff_text(svn_stream_t *wrapped_stream,
+                         apr_pool_t *result_pool)
+{
+  svn_stream_t *stream;
+  struct reverse_diff_text_stream_baton *baton;
+
+  baton = apr_palloc(result_pool, sizeof(*baton));
+  baton->wrapped_stream = wrapped_stream;
+
+  stream = svn_stream_create(baton, result_pool);
+  svn_stream_set_read(stream, read_handler_reverse_diff_text);
+  svn_stream_set_write(stream, write_handler_reverse_diff_text);
+  svn_stream_set_close(stream, close_handler_reverse_diff_text);
+  svn_stream_set_reset(stream, reset_handler_reverse_diff_text);
+  svn_stream_set_mark(stream, mark_handler_reverse_diff_text);
+  svn_stream_set_seek(stream, seek_handler_reverse_diff_text);
+  svn_stream_set_readline(stream, readline_handler_reverse_diff_text);
+
+  return stream;
+}
+
 /* Parse PROP_NAME from HEADER as the part after the INDICATOR line. */
 static svn_error_t *
 parse_prop_name(const char **prop_name, const char *header, 
@@ -349,7 +667,7 @@ parse_next_hunk(svn_hunk_t **hunk,
       /* Remember the current line's offset, and read the line. */
       last_line = pos;
       SVN_ERR(svn_stream_readline_detect_eol(stream, &line, NULL, &eof,
-                                             iterpool));
+                                             iterpool, iterpool));
 
       if (! eof)
         {
@@ -502,38 +820,41 @@ parse_next_hunk(svn_hunk_t **hunk,
       apr_file_t *f;
       apr_int32_t flags = APR_READ | APR_BUFFERED;
 
+      /* For each of the streams created below, we re-open the patch file.
+       * Each stream needs its own file descriptor in order to have
+       * independent seek behaviour. */
+
       /* Create a stream which returns the hunk text itself. */
       SVN_ERR(svn_io_file_open(&f, patch->path, flags, APR_OS_DEFAULT,
                                result_pool));
       diff_text = svn_stream_from_aprfile_range_readonly(f, FALSE,
                                                          start, end,
                                                          result_pool);
+      if (reverse)
+        diff_text = stream_reverse_diff_text(diff_text, result_pool);
 
       /* Create a stream which returns the original hunk text. */
       SVN_ERR(svn_io_file_open(&f, patch->path, flags, APR_OS_DEFAULT,
                                result_pool));
-      original_text = svn_stream_from_aprfile_range_readonly(f, FALSE,
-                                                             start, end,
-                                                             result_pool);
-      svn_stream_set_line_filter_callback(original_text, original_line_filter);
-      svn_stream_set_line_transformer_callback(original_text,
-                                               remove_leading_char_transformer);
+      original_text = stream_hunk_text_original(
+                        svn_stream_from_aprfile_range_readonly(f, FALSE,
+                                                               start, end,
+                                                               result_pool),
+                        result_pool);
 
       /* Create a stream which returns the modified hunk text. */
       SVN_ERR(svn_io_file_open(&f, patch->path, flags, APR_OS_DEFAULT,
                                result_pool));
-      modified_text = svn_stream_from_aprfile_range_readonly(f, FALSE,
-                                                             start, end,
-                                                             result_pool);
-      svn_stream_set_line_filter_callback(modified_text, modified_line_filter);
-      svn_stream_set_line_transformer_callback(modified_text,
-                                               remove_leading_char_transformer);
+      modified_text = stream_hunk_text_modified(
+                        svn_stream_from_aprfile_range_readonly(f, FALSE,
+                                                               start, end,
+                                                               result_pool),
+                        result_pool);
+
       /* Set the hunk's texts. */
       (*hunk)->diff_text = diff_text;
       if (reverse)
         {
-          svn_stream_set_line_transformer_callback(diff_text,
-                                                   reverse_diff_transformer);
           (*hunk)->original_text = modified_text;
           (*hunk)->modified_text = original_text;
         }
@@ -918,7 +1239,7 @@ svn_diff_parse_next_patch(svn_patch_t **patch,
       /* Remember the current line's offset, and read the line. */
       last_line = pos;
       SVN_ERR(svn_stream_readline_detect_eol(stream, &line, NULL, &eof,
-                                             iterpool));
+                                             iterpool, iterpool));
 
       if (! eof)
         {
Index: subversion/libsvn_subr/stream.c
===================================================================
--- subversion/libsvn_subr/stream.c	(revision 961349)
+++ subversion/libsvn_subr/stream.c	(working copy)
@@ -53,8 +53,7 @@ struct svn_stream_t {
   svn_io_reset_fn_t reset_fn;
   svn_io_mark_fn_t mark_fn;
   svn_io_seek_fn_t seek_fn;
-  svn_io_line_filter_cb_t line_filter_cb;
-  svn_io_line_transformer_cb_t line_transformer_cb;
+  svn_io_readline_fn_t readline_fn;
 };
 
 
@@ -73,8 +72,7 @@ svn_stream_create(void *baton, apr_pool_t *pool)
   stream->reset_fn = NULL;
   stream->mark_fn = NULL;
   stream->seek_fn = NULL;
-  stream->line_filter_cb = NULL;
-  stream->line_transformer_cb = NULL;
+  stream->readline_fn = NULL;
   return stream;
 }
 
@@ -124,20 +122,11 @@ svn_stream_set_seek(svn_stream_t *stream, svn_io_s
 }
 
 void
-svn_stream_set_line_filter_callback(svn_stream_t *stream,
-                                    svn_io_line_filter_cb_t line_filter_cb)
+svn_stream_set_readline(svn_stream_t *stream, svn_io_readline_fn_t readline_fn)
 {
-  stream->line_filter_cb = line_filter_cb;
+  stream->readline_fn = readline_fn;
 }
 
-void
-svn_stream_set_line_transformer_callback(
-  svn_stream_t *stream,
-  svn_io_line_transformer_cb_t line_transformer_cb)
-{
-  stream->line_transformer_cb = line_transformer_cb;
-}
-
 svn_error_t *
 svn_stream_read(svn_stream_t *stream, char *buffer, apr_size_t *len)
 {
@@ -233,55 +222,23 @@ svn_stream_printf_from_utf8(svn_stream_t *stream,
   return svn_stream_write(stream, translated, &len);
 }
 
-/* If a line filter callback was set on STREAM, invoke it on LINE,
- * and indicate in *FILTERED whether the line should be filtered.
- * If no line filter callback was set on STREAM, just set *FILTERED to FALSE.
- */
-static svn_error_t *
-line_filter(svn_stream_t *stream, svn_boolean_t *filtered, const char *line,
-            apr_pool_t *pool)
-{
-  apr_pool_t *scratch_pool;
-
-  if (! stream->line_filter_cb)
-    {
-      *filtered = FALSE;
-      return SVN_NO_ERROR;
-    }
-
-  scratch_pool = svn_pool_create(pool);
-  SVN_ERR(stream->line_filter_cb(filtered, line, stream->baton, scratch_pool));
-  svn_pool_destroy(scratch_pool);
-  return SVN_NO_ERROR;
-}
-
-/* Run the line transformer callback of STREAM with LINE as input,
- * and expect the transformation result to be returned in BUF,
- * allocated in POOL. */
-static svn_error_t *
-line_transformer(svn_stream_t *stream, svn_stringbuf_t **buf,
-                 const char *line, apr_pool_t *pool, apr_pool_t *scratch_pool)
-{
-  *buf = NULL;  /* so we can assert that the callback has set it non-null */
-  SVN_ERR(stream->line_transformer_cb(buf, line, stream->baton,
-                                      pool, scratch_pool));
-
-  /* Die if the line transformer didn't provide any output. */
-  SVN_ERR_ASSERT(*buf);
-
-  return SVN_NO_ERROR;
-}
-
-/* Scan STREAM for an end-of-line indicatior, and return it in *EOL.
+/* Invoke the READ_FN function of a stream to scan  for an end-of-line
+ * indicatior, and return it in *EOL.
+ * Use MARK_FN and SEEK_FN to seek in the stream.
  * Set *EOL to NULL if the stream runs out before an end-of-line indicator
  * is found. */
 static svn_error_t *
-scan_eol(const char **eol, svn_stream_t *stream, apr_pool_t *pool)
+scan_eol(void *baton,
+         svn_read_fn_t read_fn,
+         svn_io_mark_fn_t mark_fn,
+         svn_io_seek_fn_t seek_fn,
+         const char **eol,
+         apr_pool_t *pool)
 {
   const char *eol_str;
   svn_stream_mark_t *mark;
 
-  SVN_ERR(svn_stream_mark(stream, &mark, pool));
+  SVN_ERR(mark_fn(baton, &mark, pool));
 
   eol_str = NULL;
   while (! eol_str)
@@ -290,113 +247,82 @@ static svn_error_t *
       apr_size_t len;
 
       len = sizeof(buf);
-      SVN_ERR(svn_stream_read(stream, buf, &len));
+      SVN_ERR(read_fn(baton, buf, &len));
       if (len == 0)
           break; /* EOF */
       eol_str = svn_eol__detect_eol(buf, buf + len);
     }
 
-  SVN_ERR(svn_stream_seek(stream, mark));
+  SVN_ERR(seek_fn(baton, mark));
 
   *eol = eol_str;
 
   return SVN_NO_ERROR;
 }
 
-/* Guts of svn_stream_readline() and svn_stream_readline_detect_eol().
- * Returns the line read from STREAM in *STRINGBUF, and indicates
- * end-of-file in *EOF.  If DETECT_EOL is TRUE, the end-of-line indicator
- * is detected automatically and returned in *EOL.
- * If DETECT_EOL is FALSE, *EOL must point to the desired end-of-line
- * indicator.  STRINGBUF is allocated in POOL. */
+/* An implementation of svn_io_readline_fn_t */
 static svn_error_t *
-stream_readline(svn_stringbuf_t **stringbuf,
-                svn_boolean_t *eof,
+stream_readline(void *baton,
+                svn_stringbuf_t **stringbuf,
                 const char **eol,
-                svn_stream_t *stream,
+                svn_boolean_t *eof,
                 svn_boolean_t detect_eol,
-                apr_pool_t *pool)
+                svn_read_fn_t read_fn,
+                svn_io_mark_fn_t mark_fn,
+                svn_io_seek_fn_t seek_fn,
+                apr_pool_t *result_pool,
+                apr_pool_t *scratch_pool)
 {
-  svn_stringbuf_t *str;
-  apr_pool_t *iterpool;
-  svn_boolean_t filtered;
+  apr_size_t numbytes;
+  const char *match;
+  char c;
   const char *eol_str;
+  /* Since we're reading one character at a time, let's at least
+     optimize for the 90% case.  90% of the time, we can avoid the
+     stringbuf ever having to realloc() itself if we start it out at
+     80 chars.  */
+  svn_stringbuf_t *str = svn_stringbuf_create_ensure(80, scratch_pool);
 
-  *eof = FALSE;
-
-  iterpool = svn_pool_create(pool);
-  do
+  if (detect_eol)
     {
-      apr_size_t numbytes;
-      const char *match;
-      char c;
-
-      svn_pool_clear(iterpool);
-
-      /* Since we're reading one character at a time, let's at least
-         optimize for the 90% case.  90% of the time, we can avoid the
-         stringbuf ever having to realloc() itself if we start it out at
-         80 chars.  */
-      str = svn_stringbuf_create_ensure(80, iterpool);
-
-      if (detect_eol)
+      SVN_ERR(scan_eol(baton, read_fn, mark_fn, seek_fn,
+                       &eol_str, scratch_pool));
+      if (eol)
+        *eol = eol_str;
+      if (eol_str == NULL)
         {
-          SVN_ERR(scan_eol(&eol_str, stream, iterpool));
-          if (eol)
-            *eol = eol_str;
-          if (! eol_str)
-            {
-              /* No newline until EOF, EOL_STR can be anything. */
-              eol_str = APR_EOL_STR;
-            }
+          /* No newline until EOF, EOL_STR can be anything. */
+          eol_str = APR_EOL_STR;
         }
-      else
-        eol_str = *eol;
+    }
+  else
+    eol_str = *eol;
 
-      /* Read into STR up to and including the next EOL sequence. */
-      match = eol_str;
+  /* Read into STR up to and including the next EOL sequence. */
+  match = eol_str;
+  while (*match)
+    {
       numbytes = 1;
-      while (*match)
+      SVN_ERR(read_fn(baton, &c, &numbytes));
+      if (numbytes != 1)
         {
-          SVN_ERR(svn_stream_read(stream, &c, &numbytes));
-          if (numbytes != 1)
-            {
-              /* a 'short' read means the stream has run out. */
-              *eof = TRUE;
-              /* We know we don't have a whole EOL sequence, but ensure we
-               * don't chop off any partial EOL sequence that we may have. */
-              match = eol_str;
-              /* Process this short (or empty) line just like any other
-               * except with *EOF set. */
-              break;
-            }
-
-          if (c == *match)
-            match++;
-          else
-            match = eol_str;
-
-          svn_stringbuf_appendbytes(str, &c, 1);
+          /* a 'short' read means the stream has run out. */
+          *eof = TRUE;
+          *stringbuf = svn_stringbuf_dup(str, result_pool);
+          return SVN_NO_ERROR;
         }
 
-      svn_stringbuf_chop(str, match - eol_str);
+      if (c == *match)
+        match++;
+      else
+        match = eol_str;
 
-      SVN_ERR(line_filter(stream, &filtered, str->data, iterpool));
+      svn_stringbuf_appendbytes(str, &c, 1);
     }
-  while (filtered && ! *eof);
-  /* Not destroying the iterpool just yet since we still need STR
-   * which is allocated in it. */
 
-  if (filtered)
-    *stringbuf = svn_stringbuf_create_ensure(0, pool);
-  else if (stream->line_transformer_cb)
-    SVN_ERR(line_transformer(stream, stringbuf, str->data, pool, iterpool));
-  else
-    *stringbuf = svn_stringbuf_dup(str, pool);
-
-  /* Done. RIP iterpool. */
-  svn_pool_destroy(iterpool);
-
+  *eof = FALSE;
+  svn_stringbuf_chop(str, match - eol_str);
+  *stringbuf = svn_stringbuf_dup(str, result_pool);
   return SVN_NO_ERROR;
 }
 
@@ -407,8 +333,23 @@ svn_stream_readline(svn_stream_t *stream,
                     svn_boolean_t *eof,
                     apr_pool_t *pool)
 {
-  return svn_error_return(stream_readline(stringbuf, eof, &eol, stream,
-                                          FALSE, pool));
+  svn_io_readline_fn_t readline_fn;
+
+  /* Provide a default readline implementation if the stream
+   * hasn't overridden it. This is needed for backwards compat
+   * to 1.6.x and earlier. */
+  if (stream->readline_fn)
+    readline_fn = stream->readline_fn;
+  else
+    readline_fn = stream_readline;
+
+  return svn_error_return(readline_fn(stream->baton,
+                                      stringbuf, &eol, eof,
+                                      FALSE,
+                                      stream->read_fn,
+                                      stream->mark_fn,
+                                      stream->seek_fn,
+                                      pool, pool));
 }
 
 svn_error_t *
@@ -416,13 +357,36 @@ svn_stream_readline_detect_eol(svn_stream_t *strea
                                svn_stringbuf_t **stringbuf,
                                const char **eol,
                                svn_boolean_t *eof,
-                               apr_pool_t *pool)
+                               apr_pool_t *result_pool,
+                               apr_pool_t *scratch_pool)
 {
-  return svn_error_return(stream_readline(stringbuf, eof, eol, stream,
-                                          TRUE, pool));
-}
+  svn_io_readline_fn_t readline_fn;
 
+  /* Provide a default readline implementation if the stream
+   * hasn't overridden it. This is not needed for backwards compat
+   * to 1.6.x and earlier (this function is new in 1.7), but it
+   * is nice anyway because it saves us from adding dummy readline
+   * methods to custom streams sprinkled throughout the code. */
+  if (stream->readline_fn)
+    readline_fn = stream->readline_fn;
+  else
+    readline_fn = stream_readline;
 
+  /* EOL-detection requires mark/seek support. */
+  if (stream->mark_fn == NULL)
+    return svn_error_create(SVN_ERR_STREAM_SEEK_NOT_SUPPORTED, NULL, NULL);
+  if (stream->seek_fn == NULL)
+    return svn_error_create(SVN_ERR_STREAM_SEEK_NOT_SUPPORTED, NULL, NULL);
+
+  return svn_error_return(readline_fn(stream->baton,
+                                      stringbuf, eol, eof,
+                                      TRUE,
+                                      stream->read_fn,
+                                      stream->mark_fn,
+                                      stream->seek_fn,
+                                      result_pool, scratch_pool));
+}
+
 svn_error_t *svn_stream_copy3(svn_stream_t *from, svn_stream_t *to,
                               svn_cancel_func_t cancel_func,
                               void *cancel_baton,
@@ -536,7 +500,27 @@ seek_handler_empty(void *baton, svn_stream_mark_t
   return SVN_NO_ERROR;
 }
 
+static svn_error_t *
+readline_handler_empty(void *baton,
+                       svn_read_fn_t read_fn,
+                       svn_io_mark_fn_t mark_fn,
+                       svn_io_seek_fn_t seek_fn,
+                       svn_stringbuf_t **stringbuf,
+                       const char **eol,
+                       svn_boolean_t *eof,
+                       apr_pool_t *result_pool,
+                       apr_pool_t *scratch_pool)
+{
+  *stringbuf = svn_stringbuf_create_ensure(0, result_pool);
 
+  if (eol && *eol == NULL)
+    *eol = APR_EOL_STR;
+
+  *eof = TRUE;
+
+  return SVN_NO_ERROR;
+}
+
 svn_stream_t *
 svn_stream_empty(apr_pool_t *pool)
 {
@@ -548,6 +532,8 @@ svn_stream_empty(apr_pool_t *pool)
   svn_stream_set_reset(stream, reset_handler_empty);
   svn_stream_set_mark(stream, mark_handler_empty);
   svn_stream_set_seek(stream, seek_handler_empty);
+  svn_stream_set_readline(stream, readline_handler_empty);
+
   return stream;
 }
 
@@ -652,6 +638,7 @@ svn_stream_disown(svn_stream_t *stream, apr_pool_t
   svn_stream_set_reset(s, reset_handler_disown);
   svn_stream_set_mark(s, mark_handler_disown);
   svn_stream_set_seek(s, seek_handler_disown);
+  svn_stream_set_readline(s, stream_readline);
 
   return s;
 }
@@ -820,6 +807,7 @@ svn_stream_from_aprfile2(apr_file_t *file,
   svn_stream_set_reset(stream, reset_handler_apr);
   svn_stream_set_mark(stream, mark_handler_apr);
   svn_stream_set_seek(stream, seek_handler_apr);
+  svn_stream_set_readline(stream, stream_readline);
 
   if (! disown)
     svn_stream_set_close(stream, close_handler_apr);
@@ -898,6 +886,7 @@ svn_stream_from_aprfile_range_readonly(apr_file_t
   svn_stream_set_reset(stream, reset_handler_apr);
   svn_stream_set_mark(stream, mark_handler_apr);
   svn_stream_set_seek(stream, seek_handler_apr);
+  svn_stream_set_readline(stream, stream_readline);
 
   if (! disown)
     svn_stream_set_close(stream, close_handler_apr);
@@ -1187,6 +1176,7 @@ svn_stream_compressed(svn_stream_t *stream, apr_po
   svn_stream_set_read(zstream, read_handler_gz);
   svn_stream_set_write(zstream, write_handler_gz);
   svn_stream_set_close(zstream, close_handler_gz);
+  svn_stream_set_readline(zstream, stream_readline);
 
   return zstream;
 }
@@ -1302,6 +1292,8 @@ svn_stream_checksummed2(svn_stream_t *stream,
   svn_stream_set_read(s, read_handler_checksum);
   svn_stream_set_write(s, write_handler_checksum);
   svn_stream_set_close(s, close_handler_checksum);
+  svn_stream_set_readline(s, stream_readline);
+
   return s;
 }
 
@@ -1384,6 +1376,8 @@ svn_stream_checksummed(svn_stream_t *stream,
   svn_stream_set_read(s, read_handler_md5);
   svn_stream_set_write(s, write_handler_md5);
   svn_stream_set_close(s, close_handler_md5);
+  svn_stream_set_readline(s, stream_readline);
+
   return s;
 }
 
@@ -1475,6 +1469,8 @@ svn_stream_from_stringbuf(svn_stringbuf_t *str,
   svn_stream_set_reset(stream, reset_handler_stringbuf);
   svn_stream_set_mark(stream, mark_handler_stringbuf);
   svn_stream_set_seek(stream, seek_handler_stringbuf);
+  svn_stream_set_readline(stream, stream_readline);
+
   return stream;
 }
 
@@ -1511,6 +1507,8 @@ svn_stream_from_string(const svn_string_t *str,
   baton->amt_read = 0;
   stream = svn_stream_create(baton, pool);
   svn_stream_set_read(stream, read_handler_string);
+  svn_stream_set_readline(stream, stream_readline);
+
   return stream;
 }
 
Index: subversion/libsvn_client/patch.c
===================================================================
--- subversion/libsvn_client/patch.c	(revision 961349)
+++ subversion/libsvn_client/patch.c	(working copy)
@@ -532,7 +532,7 @@ read_line(patch_target_t *target,
 
   SVN_ERR(svn_stream_readline_detect_eol(target->stream, &line_raw,
                                          &eol_str, &target->eof,
-                                         scratch_pool));
+                                         scratch_pool, scratch_pool));
   if (target->eol_style == svn_subst_eol_style_none)
     target->eol_str = eol_str;
 
@@ -632,7 +632,8 @@ match_hunk(svn_boolean_t *matched, patch_target_t
 
       SVN_ERR(svn_stream_readline_detect_eol(hunk->original_text,
                                              &hunk_line, NULL,
-                                             &hunk_eof, iterpool));
+                                             &hunk_eof,
+                                             iterpool, iterpool));
       /* Contract keywords, if any, before matching. */
       SVN_ERR(svn_subst_translate_cstring2(hunk_line->data,
                                            &hunk_line_translated,
@@ -676,7 +677,8 @@ match_hunk(svn_boolean_t *matched, patch_target_t
       /* If the target has no newline at end-of-file, we get an EOF
        * indication for the target earlier than we do get it for the hunk. */
       SVN_ERR(svn_stream_readline_detect_eol(hunk->original_text, &hunk_line,
-                                             NULL, &hunk_eof, iterpool));
+                                             NULL, &hunk_eof,
+                                             iterpool, iterpool));
       if (hunk_line->len == 0 && hunk_eof)
         *matched = lines_matched;
       else
@@ -928,7 +930,8 @@ reject_hunk(patch_target_t *target, hunk_info_t *h
       svn_pool_clear(iterpool);
 
       SVN_ERR(svn_stream_readline_detect_eol(hi->hunk->diff_text, &hunk_line,
-                                             &eol_str, &eof, iterpool));
+                                             &eol_str, &eof, iterpool,
+                                             iterpool));
       if (! eof)
         {
           if (hunk_line->len >= 1)
@@ -994,7 +997,7 @@ apply_hunk(patch_target_t *target, hunk_info_t *hi
 
       SVN_ERR(svn_stream_readline_detect_eol(hi->hunk->modified_text,
                                              &hunk_line, &eol_str,
-                                             &eof, iterpool));
+                                             &eof, iterpool, iterpool));
       lines_read++;
       if (! eof && lines_read > hi->fuzz &&
           lines_read <= hi->hunk->modified_length - hi->fuzz)
Index: subversion/tests/libsvn_subr/stream-test.c
===================================================================
--- subversion/tests/libsvn_subr/stream-test.c	(revision 961349)
+++ subversion/tests/libsvn_subr/stream-test.c	(working copy)
@@ -307,143 +307,7 @@ test_stream_range(apr_pool_t *pool)
     return SVN_NO_ERROR;
 }
 
-/* An implementation of svn_io_line_filter_cb_t */
 static svn_error_t *
-line_filter(svn_boolean_t *filtered, const char *line, void *baton,
-            apr_pool_t *scratch_pool)
-{
-  *filtered = strchr(line, '!') != NULL;
-  return SVN_NO_ERROR;
-}
-
-static svn_error_t *
-test_stream_line_filter(apr_pool_t *pool)
-{
-  static const char *lines[4] = {"Not filtered.", "Filtered!",
-                                 "Not filtered either.", "End of the lines!"};
-  svn_string_t *string;
-  svn_stream_t *stream;
-  svn_stringbuf_t *line;
-  svn_boolean_t eof;
-
-  string = svn_string_createf(pool, "%s\n%s\n%s\n%s", lines[0], lines[1],
-                              lines[2], lines[3]);
-  stream = svn_stream_from_string(string, pool);
-
-  svn_stream_set_line_filter_callback(stream, line_filter);
-
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, lines[0]);
-  /* line[1] should be filtered */
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, lines[2]);
-
-  /* The last line should also be filtered, and the resulting
-   * stringbuf should be empty. */
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_ASSERT(eof && svn_stringbuf_isempty(line));
-
-  return SVN_NO_ERROR;
-}
-
-/* An implementation of svn_io_line_transformer_cb_t */
-static svn_error_t *
-line_transformer(svn_stringbuf_t **buf, const char *line, void *baton,
-                 apr_pool_t *result_pool, apr_pool_t *scratch_pool)
-{
-  int i, len = strlen(line);
-  char *temp = apr_palloc(scratch_pool, len + 1 );
-
-  for (i = 0; i < len; i++)
-    {
-      temp[i] = line[len - 1 - i];
-    }
-
-  temp[len] = '\0';
-
-  *buf = svn_stringbuf_create(temp, result_pool);
-
-  return SVN_NO_ERROR;
-}
-
-static svn_error_t *
-test_stream_line_transformer(apr_pool_t *pool)
-{
-  static const char *lines[4] = {"gamma", "",
-                                 "iota", "!"};
-
-  static const char *inv_lines[4] = {"ammag", "",
-                                 "atoi", "!"};
-  svn_string_t *string;
-  svn_stream_t *stream;
-  svn_stringbuf_t *line;
-  svn_boolean_t eof;
-
-  string = svn_string_createf(pool, "%s\n%s\n%s\n%s", lines[0], lines[1],
-                              lines[2], lines[3]);
-
-  stream = svn_stream_from_string(string, pool);
-
-  svn_stream_set_line_transformer_callback(stream, line_transformer);
-
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, inv_lines[0]);
-
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, inv_lines[1]);
-
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, inv_lines[2]);
-
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, inv_lines[3]);
-
-  /* We should have reached eof and the stringbuf should be emtpy. */
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_ASSERT(eof && svn_stringbuf_isempty(line));
-
-  return SVN_NO_ERROR;
-}
-
-static svn_error_t *
-test_stream_line_filter_and_transformer(apr_pool_t *pool)
-{
-  static const char *lines[4] = {"!gamma", "",
-                                 "iota", "!"};
-
-  static const char *inv_lines[4] = {"ammag", "",
-                                 "atoi", "!"};
-  svn_string_t *string;
-  svn_stream_t *stream;
-  svn_stringbuf_t *line;
-  svn_boolean_t eof;
-
-  string = svn_string_createf(pool, "%s\n%s\n%s\n%s", lines[0], lines[1],
-                              lines[2], lines[3]);
-
-  stream = svn_stream_from_string(string, pool);
-
-  svn_stream_set_line_filter_callback(stream, line_filter);
-
-  svn_stream_set_line_transformer_callback(stream, line_transformer);
-
-  /* Line one should be filtered. */
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, inv_lines[1]);
-
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_STRING_ASSERT(line->data, inv_lines[2]);
-
-  /* The last line should also be filtered, and the resulting
-   * stringbuf should be empty. */
-  svn_stream_readline(stream, &line, "\n", &eof, pool);
-  SVN_TEST_ASSERT(eof && svn_stringbuf_isempty(line));
-
-  return SVN_NO_ERROR;
-
-}
-
-static svn_error_t *
 test_stream_tee(apr_pool_t *pool)
 {
   svn_stringbuf_t *test_bytes = generate_test_bytes(100, pool);
@@ -647,12 +511,6 @@ struct svn_test_descriptor_t test_funcs[] =
                    "test compressed streams"),
     SVN_TEST_PASS2(test_stream_range,
                    "test streams reading from range of file"),
-    SVN_TEST_PASS2(test_stream_line_filter,
-                   "test stream line filtering"),
-    SVN_TEST_PASS2(test_stream_line_transformer,
-                   "test stream line transforming"),
-    SVN_TEST_PASS2(test_stream_line_filter_and_transformer,
-                   "test stream line filtering and transforming"),
     SVN_TEST_PASS2(test_stream_tee,
                    "test 'tee' streams"),
     SVN_TEST_PASS2(test_stream_seek_file,


Re: [PATCH] issue #3555: make svn_stream_readline() a stream method

Posted by Julian Foad <ju...@wandisco.com>.
Hi Stefan.  I've just started looking at this.  Overall impression is it
looks like you've done it very well - thank you!

Could you possibly get the readline_detect_eol() API changes out of this
patch, and make them in a separate patch either before or after this?
Or if that change belongs in this patch, please explain why in the log
msg.

Thanks.
- Julian


On Wed, 2010-07-07 at 20:39 +0200, Stefan Sperling wrote:
> This change was proposed by Julian some time ago, because having
> callback types that change the behaviour of svn_stream_readline(),
> namely svn_io_line_filter_cb_t and svn_io_line_transformer_cb_t,
> was the wrong approach.
> See http://subversion.tigris.org/issues/show_bug.cgi?id=3555
> 
> The patch below implements Julian's second suggestion from the issue:
> "make readline() a virtual method of svn_stream_t and then have the user
> provide replacement implementations of readline() for doing the required
> filtering."
> 
> One caveat is that to remain backwards compatible we cannot require
> streams to set a readline method. So svn_stream_readline() falls back
> to the default readline implementation if none has been set.
> 
> I have taken a shortcut and made svn_stream_readline_detect_eol() (which
> is new in 1.7) also provide a readline fallback. This avoids having to
> add dummy readline methods to streams elsewhere in our code, such as the
> one in fs_fs.c.
> 
> svn_stream_readline_detect_eol() now behaves like svn_stream_readline()
> if an EOL character is passed in.
> I briefly considered deprecating svn_stream_readline() and renaming
> svn_stream_readline_detect_eol() to svn_stream_readline2(), but
> decided against doing so as it would cause a lot of code churn and
> existing callers of svn_stream_readline() won't benefit from the upgrade.
> 
> The readline callbacks were initially added to address the needs of svn
> patch, in particularly the diff parser. The parser now has two custom readline
> implementations. There is some code duplication between implementations.
> But the generic readline method becomes a bit simpler again. It's not as
> simple as it was in 1.6.x, because we need a way to read lines with arbitrary
> EOL characters. The diffstat illustrates nicely how code has shifted around:
> 
> 
>  include/svn_io.h                |   97 +++-----
>  libsvn_client/patch.c           |   16 -
>  libsvn_diff/parse-diff.c        |  452 +++++++++++++++++++++++++++++++++-------
>  libsvn_subr/stream.c            |  285 ++++++++++++-------------
>  tests/libsvn_subr/stream-test.c |  142 ------------
>  5 files changed, 572 insertions(+), 420 deletions(-)
> 
> The patch passes make check.
> I'm posting it here for review by Julian and others who are interested.
> 
> Thanks,
> Stefan
> 
> [[[
> Fix issue #3555, 'Remove the "line_filter" and "line_transformer" callbacks
> from svn_stream_t'.
> 
> Make svn_stream_readline() a virtual method of svn_stream_t,
> and provide custom implementations of readline methods in the diff
> parsing code (which is the only place where we need this right now).
> 
> * subversion/include/svn_io.h
>   (svn_io_line_filter_cb_t, svn_io_line_transformer_cb_t,
>    svn_stream_set_line_filter_callback,
>    svn_stream_set_line_transformer_callback): Remove.
>   (svn_io_readline_fn_t, svn_stream_set_readline): Declare.
>   (svn_stream_readline): Adjust docstring.
>   (svn_stream_readline_detect_eol): Switch to dual-pool paradigm,
>    and document the new twisted behaviour of the EOL input/output parameter.
> 
> * subversion/libsvn_diff/parse-diff.c
>   (original_line_filter, modified_line_filter, remove_leading_char_transformer,
>    reverse_diff_transformer): Remove.
>   (hunk_text_stream_baton, read_handler_hunk_text, write_handler_hunk_text,
>    close_handler_hunk_text, reset_handler_hunk_text, mark_handler_hunk_text,
>    seek_handler_hunk_text, scan_eol, readline_handler_hunk_text,
>    stream_hunk_text, stream_hunk_text_original, stream_hunk_text_modified):
>     New. Implementation of a stream which provides a special readline
>     method to read original/modified texts of hunks from a patch file stream.
>   (reverse_diff_text_stream_baton, read_handler_reverse_diff_text,
>    write_handler_reverse_diff_text, close_handler_reverse_diff_text,
>    reset_handler_reverse_diff_text, mark_handler_reverse_diff_text,
>    seek_handler_reverse_diff_text, readline_handler_reverse_diff_text,
>    stream_reverse_diff_text): New. Implementation of a stream which
>     provides a special readline method which reverses unidiff data read
>     from the wrapped stream.
>   (parse_next_hunk): Track svn_stream_readline_detect_eol() dual-pool change.
>    Add a comment explaining why the patch file gets opened multiple
>    times (drive-by fix because this confused me at first).
>    Instead of installing line-filter/transformation callbacks on
>    streams, wrap streams with appropriate wrapper streams.
> 
> * subversion/libsvn_subr/stream.c
>   (struct svn_stream_t): Replace line_filter_cb and line_transformer_cb
>    members with readline_fn member.
>   (svn_stream_create): Track changes to svn_stream_t.
>   (svn_stream_set_line_filter_callback,
>    svn_stream_set_line_transformer_callback,
>    line_filter, line_transformer): Remove.
>   (scan_eol): Tweak argument list for use within a stream method.
>    This function can no longer expect a stream, so pass a baton
>    and a set of required stream methods instead.
>   (stream_readline): Make this in svn_io_readline_fn_t implementation.
>    Remove handling of line filters/transformers.
>   (svn_stream_readline): Instead of calling the stream_readline() helper
>    directly, call a custom readline implementation if one is set on the
>    stream. If no custom implementation is provided, fall back to the
>    stream_readline() helper function to preserve compatibility with 1.6.x.
>    Assert that the caller passed an EOL character, because the new
>    svn_io_readline_fn_t does not strictly require it.
>   (svn_stream_readline_detect_eol): As previous, but ensure that
>    the stream has mark/seek support instead of asserting an EOL.
>   (readline_handler_empty): Custom readline handler for the empty stream.
>   (svn_stream_empty, svn_stream_disown, svn_stream_from_aprfile2,
>    svn_stream_from_aprfile_range_readonly, svn_stream_compressed,
>    svn_stream_checksummed2, svn_stream_checksummed, svn_stream_from_stringbuf,
>    svn_stream_from_string): Set a readline method.
> 
> * subversion/libsvn_client/patch.c
>   (read_line, match_hunk, reject_hunk, apply_hunk): Ensure that EOL_STR
>     is always initialised to fulfill new API requirements of
>     svn_stream_readline_detect_eol regarding EOLs.
>     Also pass two pools to svn_stream_readline_detect_eol().
> 
> * subversion/tests/libsvn_subr/stream-test.c
>   (line_filter, test_stream_line_filter, line_transformer,
>    test_stream_line_transformer,
>    test_stream_line_filter_and_transformer): Remove these tests.
>   (test_funcs): Remove removed tests.
> 
> ]]]
> 
> 
> Index: subversion/include/svn_io.h
> ===================================================================
> --- subversion/include/svn_io.h	(revision 961349)
> +++ subversion/include/svn_io.h	(working copy)
> @@ -780,40 +780,36 @@ typedef svn_error_t *(*svn_io_mark_fn_t)(void *bat
>  typedef svn_error_t *(*svn_io_seek_fn_t)(void *baton,
>                                           svn_stream_mark_t *mark);
>  
> -/** Line-filtering callback function for a generic stream.
> - * @a baton is the stream's baton.
> - * @see svn_stream_t, svn_stream_set_baton() and svn_stream_readline().
> +/** Line-reading handler function for a generic stream.
>   *
> - * @since New in 1.7.
> - */
> -typedef svn_error_t *(*svn_io_line_filter_cb_t)(svn_boolean_t *filtered,
> -                                                const char *line,
> -                                                void *baton,
> -                                                apr_pool_t *scratch_pool);
> -
> -/** A callback function, invoked by svn_stream_readline(), which can perform
> - * arbitary transformations on the line before it is passed back to the caller
> - * of svn_stream_readline().
> + * Allocate @a *stringbuf in @a result_pool, and read into it one line
> + * from @a stream. If @a *eol is not NULL, it is used as the expected
> + * line terminator. If @a eol is NULL, the line-terminator is detected
> + * automatically. If @a *eol is NULL, the line-terminator is detected
> + * automatically and is returned in @a *eol.
>   *
> - * Returns a transformed stringbuf in @a buf, allocated in @a result_pool.
> - * This callback gets invoked on lines which were not filtered by the
> - * line-filtering callback function.
> + * @a mark_fn and @a seek_fn are required to be non-NULL if the end-of-line
> + * indicator is to be detected automatically. Else, they may be NULL.
>   *
> - * Implementations should always at least return an empty stringbuf.
> - * It is a fatal error if an implementation returns @a *buf as NULL.
> + * The line-terminator is read from the stream, but is not added to
> + * the end of the stringbuf.  Instead, the stringbuf ends with a usual '\\0'.
>   *
> - * @a baton is the stream's baton.
> + * If @a stream runs out of bytes before encountering a line-terminator,
> + * then set @a *eof to @c TRUE, otherwise set @a *eof to FALSE.
>   *
> - * @see svn_stream_t, svn_stream_set_baton(), svn_io_line_filter_cb_t and
> - * svn_stream_readline().
> + * Temporary allocations will be performed in @a scratch_pool.
>   *
> - * @since New in 1.7.
> - */
> -typedef svn_error_t *(*svn_io_line_transformer_cb_t)(svn_stringbuf_t **buf,
> -                                                     const char *line,
> -                                                     void *baton,
> -                                                     apr_pool_t *result_pool,
> -                                                     apr_pool_t *scratch_pool);
> + * @see svn_stream_readline and svn_stream_readline_detect_eol.
> + * @since New in 1.7. */
> +typedef svn_error_t *(*svn_io_readline_fn_t)(void *baton,
> +                                             svn_read_fn_t read_fn,
> +                                             svn_io_mark_fn_t mark_fn,
> +                                             svn_io_seek_fn_t seek_fn,
> +                                             svn_stringbuf_t **stringbuf,
> +                                             const char **eol,
> +                                             svn_boolean_t *eof,
> +                                             apr_pool_t *result_pool,
> +                                             apr_pool_t *scratch_pool);
>  
>  /** Create a generic stream.  @see svn_stream_t. */
>  svn_stream_t *
> @@ -864,23 +860,11 @@ void
>  svn_stream_set_seek(svn_stream_t *stream,
>                      svn_io_seek_fn_t seek_fn);
>  
> -/** Set @a stream's line-filtering callback function to @a line_filter_cb
> - *
> - * @since New in 1.7.
> - */
> +/** Set @a stream's readline function to @a readline_fn */
>  void
> -svn_stream_set_line_filter_callback(svn_stream_t *stream,
> -                                    svn_io_line_filter_cb_t line_filter_cb);
> +svn_stream_set_readline(svn_stream_t *stream,
> +                        svn_io_readline_fn_t readline_fn);
>  
> -/** Set @a streams's line-transforming callback function to
> - * @a line_transformer_cb.
> - *
> - * @since New in 1.7.
> - */
> -void
> -svn_stream_set_line_transformer_callback(
> -  svn_stream_t *stream,
> -  svn_io_line_transformer_cb_t line_transformer_cb);
>  
>  /** Create a stream that is empty for reading and infinite for writing. */
>  svn_stream_t *
> @@ -1203,19 +1187,6 @@ svn_stream_printf_from_utf8(svn_stream_t *stream,
>   *
>   * If @a stream runs out of bytes before encountering a line-terminator,
>   * then set @a *eof to @c TRUE, otherwise set @a *eof to FALSE.
> - *
> - * If a line-filter callback function was set on the stream using
> - * svn_stream_set_line_filter_callback(), lines will only be returned
> - * if they pass the filtering decision of the callback function.
> - * If end-of-file is encountered while reading the line and the line
> - * is filtered, @a *stringbuf will be empty.
> - *
> - * If a line-transformer callback function was set on the stream using
> - * svn_stream_set_line_transformer_callback(), lines will be returned
> - * transformed, in a way determined by the callback.
> - *
> - * Note that the line-transformer callback gets called after the line-filter
> - * callback, not before.
>   */
>  svn_error_t *
>  svn_stream_readline(svn_stream_t *stream,
> @@ -1225,11 +1196,14 @@ svn_stream_readline(svn_stream_t *stream,
>                      apr_pool_t *pool);
>  
>  /**
> - * Similar to svn_stream_readline(). The line-terminator is detected
> - * automatically.  If @a eol is not NULL, the detected line-terminator
> - * is returned in @a *eol.  If EOF is reached and the stream does not
> - * end with a newline character, @a *eol will be NULL.
> + * Similar to svn_stream_readline(). If @a *eol is not NULL, it is used
> + * as the expected line terminator. If @a eol is NULL, the line-terminator
> + * is detected automatically. If @a *eol is NULL, the line-terminator is
> + * detected automatically and is returned in @a *eol.
>   *
> + * The @a *stringbuf will be allocated in @a result_pool.
> + * @a scratch_pool is used for temporary allocations.
> + * 
>   * @since New in 1.7.
>   */
>  svn_error_t *
> @@ -1237,7 +1211,8 @@ svn_stream_readline_detect_eol(svn_stream_t *strea
>                                 svn_stringbuf_t **stringbuf,
>                                 const char **eol,
>                                 svn_boolean_t *eof,
> -                               apr_pool_t *pool);
> +                               apr_pool_t *result_pool,
> +                               apr_pool_t *scratch_pool);
>  
>  /**
>   * Read the contents of the readable stream @a from and write them to the
> Index: subversion/libsvn_diff/parse-diff.c
> ===================================================================
> --- subversion/libsvn_diff/parse-diff.c	(revision 961349)
> +++ subversion/libsvn_diff/parse-diff.c	(working copy)
> @@ -33,6 +33,8 @@
>  #include "svn_dirent_uri.h"
>  #include "svn_diff.h"
>  
> +#include "private/svn_eol_private.h"
> +
>  /* Helper macro for readability */
>  #define starts_with(str, start)  \
>    (strncmp((str), (start), strlen(start)) == 0)
> @@ -184,89 +186,398 @@ parse_hunk_header(const char *header, svn_hunk_t *
>    return TRUE;
>  }
>  
> -/* A stream line-filter which allows only original text from a hunk,
> - * and filters special lines (which start with a backslash). */
> +/* Users of the diff parsing API are provided with various streams
> + * to read lines from a hunk. These streams return:
> + *
> + *  - the original hunk text (all lines starting with '-' or ' ')
> + *  - the modified hunk text (all lines starting with '+' or ' ')
> + *  - the plain unidiff text (possibly reversed)
> + *
> + * To achieve this, we wrap the patch file stream with custom streams,
> + * which override the wrapped stream's readline method. */
> +
> +/* Baton for a stream that reads hunk texts. */
> +struct hunk_text_stream_baton {
> +  /* The leading unidiff symbol of lines which should be filtered.
> +   * Can be '+' or '-', depending on whether we're providing the original
> +   * or modified version of the hunk. */
> +  char verboten;
> +
> +  svn_stream_t *wrapped_stream;
> +};
> +
> +/* An implementation of svn_read_fn_t. */
>  static svn_error_t *
> -original_line_filter(svn_boolean_t *filtered, const char *line, void *baton,
> -                     apr_pool_t *scratch_pool)
> +read_handler_hunk_text(void *baton, char *buffer, apr_size_t *len)
>  {
> -  *filtered = (line[0] == '+' || line[0] == '\\');
> -  return SVN_NO_ERROR;
> +  struct hunk_text_stream_baton *b = baton;
> +  return svn_stream_read(b->wrapped_stream, buffer, len);
>  }
>  
> -/* A stream line-filter which allows only modified text from a hunk,
> - * and filters special lines (which start with a backslash). */
> +/* An implementation of svn_write_fn_t. */
>  static svn_error_t *
> -modified_line_filter(svn_boolean_t *filtered, const char *line, void *baton,
> -                     apr_pool_t *scratch_pool)
> +write_handler_hunk_text(void *baton, const char *buffer, apr_size_t *len)
>  {
> -  *filtered = (line[0] == '-' || line[0] == '\\');
> -  return SVN_NO_ERROR;
> +  struct hunk_text_stream_baton *b = baton;
> +  return svn_stream_write(b->wrapped_stream, buffer, len);
>  }
>  
> -/** line-transformer callback to shave leading diff symbols. */
> +/* An implementation of svn_close_fn_t. */
>  static svn_error_t *
> -remove_leading_char_transformer(svn_stringbuf_t **buf,
> -                                const char *line,
> -                                void *baton,
> -                                apr_pool_t *result_pool,
> -                                apr_pool_t *scratch_pool)
> +close_handler_hunk_text(void *baton)
>  {
> -  if (line[0] == '+' || line[0] == '-' || line[0] == ' ')
> -    *buf = svn_stringbuf_create(line + 1, result_pool);
> -  else
> -    *buf = svn_stringbuf_create(line, result_pool);
> +  struct hunk_text_stream_baton *b = baton;
> +  return svn_stream_close(b->wrapped_stream);
> +}
>  
> +/* An implementation of svn_io_reset_fn_t. */
> +static svn_error_t *
> +reset_handler_hunk_text(void *baton)
> +{
> +  struct hunk_text_stream_baton *b = baton;
> +  return svn_stream_reset(b->wrapped_stream);
> +}
> +
> +/* An implementation of svn_io_mark_fn_t. */
> +static svn_error_t *
> +mark_handler_hunk_text(void *baton, svn_stream_mark_t **mark, apr_pool_t *pool)
> +{
> +  struct hunk_text_stream_baton *b = baton;
> +  return svn_stream_mark(b->wrapped_stream, mark, pool);
> +}
> +
> +/* An implementation of svn_io_seek_fn_t. */
> +static svn_error_t *
> +seek_handler_hunk_text(void *baton, svn_stream_mark_t *mark)
> +{
> +  struct hunk_text_stream_baton *b = baton;
> +  return svn_stream_seek(b->wrapped_stream, mark);
> +}
> +
> +/* Invoke the READ_FN function of a stream to scan for an end-of-line
> + * indicator, and return it in *EOL.
> + * Set *EOL to NULL if the stream runs out before an end-of-line indicator
> + * is found. */
> +static svn_error_t *
> +scan_eol(void *baton,
> +         svn_read_fn_t read_fn,
> +         svn_io_mark_fn_t mark_fn,
> +         svn_io_seek_fn_t seek_fn,
> +         const char **eol,
> +         apr_pool_t *pool)
> +{
> +  const char *eol_str;
> +  svn_stream_mark_t *mark;
> +
> +  SVN_ERR(mark_fn(baton, &mark, pool));
> +
> +  eol_str = NULL;
> +  while (! eol_str)
> +    {
> +      char buf[512];
> +      apr_size_t len;
> +
> +      len = sizeof(buf);
> +      SVN_ERR(read_fn(baton, buf, &len));
> +      if (len == 0)
> +          break; /* EOF */
> +      eol_str = svn_eol__detect_eol(buf, buf + len);
> +    }
> +
> +  SVN_ERR(seek_fn(baton, mark));
> +
> +  *eol = eol_str;
> +
>    return SVN_NO_ERROR;
>  }
>  
> -/** line-transformer callback to reverse a diff text. */
> +/* An implementation of svn_io_readline_fn_t. */
>  static svn_error_t *
> -reverse_diff_transformer(svn_stringbuf_t **buf,
> -                         const char *line,
> -                         void *baton,
> -                         apr_pool_t *result_pool,
> -                         apr_pool_t *scratch_pool)
> +readline_handler_hunk_text(void *baton,
> +                           svn_read_fn_t read_fn,
> +                           svn_io_mark_fn_t mark_fn,
> +                           svn_io_seek_fn_t seek_fn,
> +                           svn_stringbuf_t **stringbuf,
> +                           const char **eol,
> +                           svn_boolean_t *eof,
> +                           apr_pool_t *result_pool,
> +                           apr_pool_t *scratch_pool)
>  {
> -  svn_hunk_t hunk;
> +  svn_stringbuf_t *str;
> +  apr_pool_t *iterpool;
> +  svn_boolean_t filtered;
> +  const char *eol_str;
> +  struct hunk_text_stream_baton *b = baton;
>  
> -  /* ### Pass the already parsed hunk via the baton?
> -   * ### Maybe we should really make svn_stream_readline() a proper stream
> -   * ### method and override it instead of adding special callbacks? */
> -  if (parse_hunk_header(line, &hunk, "@@", FALSE, scratch_pool))
> +  *eof = FALSE;
> +
> +  iterpool = svn_pool_create(scratch_pool);
> +  do
>      {
> -      *buf = svn_stringbuf_createf(result_pool,
> -                                   "@@ -%lu,%lu +%lu,%lu @@",
> -                                   hunk.modified_start,
> -                                   hunk.modified_length,
> -                                   hunk.original_start,
> -                                   hunk.original_length);
> +      apr_size_t numbytes;
> +      const char *match;
> +      char c;
> +
> +      svn_pool_clear(iterpool);
> +
> +      /* Since we're reading one character at a time, let's at least
> +         optimize for the 90% case.  90% of the time, we can avoid the
> +         stringbuf ever having to realloc() itself if we start it out at
> +         80 chars.  */
> +      str = svn_stringbuf_create_ensure(80, iterpool);
> +
> +      if (eol == NULL || *eol == NULL)
> +        {
> +          SVN_ERR(scan_eol(baton, read_fn, mark_fn, seek_fn,
> +                           &eol_str, iterpool));
> +          if (! eol_str)
> +            {
> +              /* No newline until EOF, EOL_STR can be anything. */
> +              eol_str = APR_EOL_STR;
> +            }
> +          if (eol)
> +            *eol = eol_str;
> +        }
> +      else
> +        eol_str = *eol;
> +
> +      /* Read into STR up to and including the next EOL sequence. */
> +      match = eol_str;
> +      numbytes = 1;
> +      while (*match)
> +        {
> +          SVN_ERR(read_fn(baton, &c, &numbytes));
> +          if (numbytes != 1)
> +            {
> +              /* a 'short' read means the stream has run out. */
> +              *eof = TRUE;
> +              /* We know we don't have a whole EOL sequence, but ensure we
> +               * don't chop off any partial EOL sequence that we may have. */
> +              match = eol_str;
> +              /* Process this short (or empty) line just like any other
> +               * except with *EOF set. */
> +              break;
> +            }
> +
> +          if (c == *match)
> +            match++;
> +          else
> +            match = eol_str;
> +
> +          svn_stringbuf_appendbytes(str, &c, 1);
> +        }
> +
> +      svn_stringbuf_chop(str, match - eol_str);
> +      filtered = (str->data[0] == b->verboten || str->data[0] == '\\');
>      }
> -  else if (parse_hunk_header(line, &hunk, "##", FALSE, scratch_pool))
> +  while (filtered && ! *eof);
> +  /* Not destroying the iterpool just yet since we still need STR
> +   * which is allocated in it. */
> +
> +  if (filtered)
>      {
> -      *buf = svn_stringbuf_createf(result_pool,
> -                                   "## -%lu,%lu +%lu,%lu ##",
> -                                   hunk.modified_start,
> -                                   hunk.modified_length,
> -                                   hunk.original_start,
> -                                   hunk.original_length);
> +      /* EOF, return an empty string. */
> +      *stringbuf = svn_stringbuf_create_ensure(0, result_pool);
>      }
> -  else if (line[0] == '+')
> +  else if (str->data[0] == '+' || str->data[0] == '-' || str->data[0] == ' ')
>      {
> -      *buf = svn_stringbuf_create(line, result_pool);
> -      (*buf)->data[0] = '-';
> +      /* Shave off leading unidiff symbols. */
> +      *stringbuf = svn_stringbuf_create(str->data + 1, result_pool);
>      }
> -  else if (line[0] == '-')
> +  else
>      {
> -      *buf = svn_stringbuf_create(line, result_pool);
> -      (*buf)->data[0] = '+';
> +      /* Return the line as-is. */
> +      *stringbuf = svn_stringbuf_dup(str, result_pool);
>      }
> +
> +  /* Done. RIP iterpool. */
> +  svn_pool_destroy(iterpool);
> +
> +  return SVN_NO_ERROR;
> +}
> +
> +/* Return a stream for reading hunk text from WRAPPED_STREAM.
> + * VERBOTEN is the leading character of lines which should be
> + * filtered by this stream's readline method ('+' or '-').
> + * Allocate the new stream in RESULT_POOL. */
> +static svn_stream_t *
> +stream_hunk_text(svn_stream_t *wrapped_stream,
> +                 char verboten,
> +                 apr_pool_t *result_pool)
> +{
> +  svn_stream_t *stream;
> +  struct hunk_text_stream_baton *baton;
> +
> +  baton = apr_palloc(result_pool, sizeof(*baton));
> +  baton->wrapped_stream = wrapped_stream;
> +  baton->verboten = verboten;
> +
> +  stream = svn_stream_create(baton, result_pool);
> +
> +  svn_stream_set_read(stream, read_handler_hunk_text);
> +  svn_stream_set_write(stream, write_handler_hunk_text);
> +  svn_stream_set_close(stream, close_handler_hunk_text);
> +  svn_stream_set_reset(stream, reset_handler_hunk_text);
> +  svn_stream_set_mark(stream, mark_handler_hunk_text);
> +  svn_stream_set_seek(stream, seek_handler_hunk_text);
> +  svn_stream_set_readline(stream, readline_handler_hunk_text);
> +
> +  return stream;
> +}
> +
> +/* Return a stream to read the original text of a hunk from
> + * WRAPPED_STREAM, and allocated in RESULT_POOL.*/
> +static svn_stream_t *
> +stream_hunk_text_original(svn_stream_t *wrapped_stream,
> +                          apr_pool_t *result_pool)
> +{
> +  return stream_hunk_text(wrapped_stream, '+', result_pool);
> +}
> +
> +/* Return a stream to read the modified text of a hunk from
> + * WRAPPED_STREAM, and allocated in RESULT_POOL.*/
> +static svn_stream_t *
> +stream_hunk_text_modified(svn_stream_t *wrapped_stream,
> +                          apr_pool_t *result_pool)
> +{
> +  return stream_hunk_text(wrapped_stream, '-', result_pool);
> +}
> +
> +
> +/* Baton for a stream that reads unidiff text reversed. */
> +struct reverse_diff_text_stream_baton {
> +  svn_stream_t *wrapped_stream;
> +}; 
> +
> +/* An implementation of svn_read_fn_t. */
> +static svn_error_t *
> +read_handler_reverse_diff_text(void *baton, char *buffer, apr_size_t *len)
> +{
> +  struct reverse_diff_text_stream_baton *b = baton;
> +  return svn_stream_read(b->wrapped_stream, buffer, len);
> +}
> +
> +/* An implementation of svn_write_fn_t. */
> +static svn_error_t *
> +write_handler_reverse_diff_text(void *baton, const char *buffer,
> +                                apr_size_t *len)
> +{
> +  struct reverse_diff_text_stream_baton *b = baton;
> +  return svn_stream_write(b->wrapped_stream, buffer, len);
> +}
> +
> +/* An implementation of svn_close_fn_t. */
> +static svn_error_t *
> +close_handler_reverse_diff_text(void *baton)
> +{
> +  struct reverse_diff_text_stream_baton *b = baton;
> +  return svn_stream_close(b->wrapped_stream);
> +}
> +
> +/* An implementation of svn_io_reset_fn_t. */
> +static svn_error_t *
> +reset_handler_reverse_diff_text(void *baton)
> +{
> +  struct reverse_diff_text_stream_baton *b = baton;
> +  return svn_stream_reset(b->wrapped_stream);
> +}
> +
> +/* An implementation of svn_io_mark_fn_t. */
> +static svn_error_t *
> +mark_handler_reverse_diff_text(void *baton, svn_stream_mark_t **mark,
> +                               apr_pool_t *pool)
> +{
> +  struct reverse_diff_text_stream_baton *b = baton;
> +  return svn_stream_mark(b->wrapped_stream, mark, pool);
> +}
> +
> +/* An implementation of svn_io_seek_fn_t. */
> +static svn_error_t *
> +seek_handler_reverse_diff_text(void *baton, svn_stream_mark_t *mark)
> +{
> +  struct hunk_text_stream_baton *b = baton;
> +  return svn_stream_seek(b->wrapped_stream, mark);
> +}
> +
> +/* An implementation of svn_io_readline_fn_t. */
> +static svn_error_t *
> +readline_handler_reverse_diff_text(void *baton,
> +                                   svn_read_fn_t read_fn,
> +                                   svn_io_mark_fn_t mark_fn,
> +                                   svn_io_seek_fn_t seek_fn,
> +                                   svn_stringbuf_t **stringbuf,
> +                                   const char **eol,
> +                                   svn_boolean_t *eof,
> +                                   apr_pool_t *result_pool,
> +                                   apr_pool_t *scratch_pool)
> +{
> +  svn_hunk_t hunk;
> +  svn_stringbuf_t *line;
> +  struct reverse_diff_text_stream_baton *b = baton;
> +
> +  /* Read the line and perform necessary transformations to
> +   * produce a reversed diff. */
> +  SVN_ERR(svn_stream_readline_detect_eol(b->wrapped_stream,
> +                                         &line, eol, eof,
> +                                         result_pool, scratch_pool));
> +  if (parse_hunk_header(line->data, &hunk, "@@", FALSE, scratch_pool))
> +    {
> +      /* Line is a hunk header, reverse it. */
> +      *stringbuf = svn_stringbuf_createf(result_pool,
> +                                         "@@ -%lu,%lu +%lu,%lu @@",
> +                                         hunk.modified_start,
> +                                         hunk.modified_length,
> +                                         hunk.original_start,
> +                                         hunk.original_length);
> +    }
> +  else if (parse_hunk_header(line->data, &hunk, "##", FALSE, scratch_pool))
> +    {
> +      /* Line is a hunk header, reverse it. */
> +      *stringbuf = svn_stringbuf_createf(result_pool,
> +                                         "## -%lu,%lu +%lu,%lu ##",
> +                                         hunk.modified_start,
> +                                         hunk.modified_length,
> +                                         hunk.original_start,
> +                                         hunk.original_length);
> +    }
>    else
> -    *buf = svn_stringbuf_create(line, result_pool);
> +    {
> +      if (line->data[0] == '+')
> +        line->data[0] = '-';
> +      else if (line->data[0] == '-')
> +        line->data[0] = '+';
>  
> +      *stringbuf = line;
> +    }
> +
>    return SVN_NO_ERROR;
>  }
>  
> +/* Return a stream for reading diff text from WRAPPED_STREAM.
> + * The unidiff will appear reversed when read via the stream's readline method.
> + * Allocate the new stream in RESULT_POOL. */
> +static svn_stream_t *
> +stream_reverse_diff_text(svn_stream_t *wrapped_stream,
> +                         apr_pool_t *result_pool)
> +{
> +  svn_stream_t *stream;
> +  struct reverse_diff_text_stream_baton *baton;
> +
> +  baton = apr_palloc(result_pool, sizeof(*baton));
> +  baton->wrapped_stream = wrapped_stream;
> +
> +  stream = svn_stream_create(baton, result_pool);
> +  svn_stream_set_read(stream, read_handler_reverse_diff_text);
> +  svn_stream_set_write(stream, write_handler_reverse_diff_text);
> +  svn_stream_set_close(stream, close_handler_reverse_diff_text);
> +  svn_stream_set_reset(stream, reset_handler_reverse_diff_text);
> +  svn_stream_set_mark(stream, mark_handler_reverse_diff_text);
> +  svn_stream_set_seek(stream, seek_handler_reverse_diff_text);
> +  svn_stream_set_readline(stream, readline_handler_reverse_diff_text);
> +
> +  return stream;
> +}
> +
>  /* Parse PROP_NAME from HEADER as the part after the INDICATOR line. */
>  static svn_error_t *
>  parse_prop_name(const char **prop_name, const char *header, 
> @@ -349,7 +660,7 @@ parse_next_hunk(svn_hunk_t **hunk,
>        /* Remember the current line's offset, and read the line. */
>        last_line = pos;
>        SVN_ERR(svn_stream_readline_detect_eol(stream, &line, NULL, &eof,
> -                                             iterpool));
> +                                             iterpool, iterpool));
>  
>        if (! eof)
>          {
> @@ -502,38 +813,41 @@ parse_next_hunk(svn_hunk_t **hunk,
>        apr_file_t *f;
>        apr_int32_t flags = APR_READ | APR_BUFFERED;
>  
> +      /* For each of the streams created below, we re-open the patch file.
> +       * Each stream needs its own file descriptor in order to have
> +       * independent seek behaviour. */
> +
>        /* Create a stream which returns the hunk text itself. */
>        SVN_ERR(svn_io_file_open(&f, patch->path, flags, APR_OS_DEFAULT,
>                                 result_pool));
>        diff_text = svn_stream_from_aprfile_range_readonly(f, FALSE,
>                                                           start, end,
>                                                           result_pool);
> +      if (reverse)
> +        diff_text = stream_reverse_diff_text(diff_text, result_pool);
>  
>        /* Create a stream which returns the original hunk text. */
>        SVN_ERR(svn_io_file_open(&f, patch->path, flags, APR_OS_DEFAULT,
>                                 result_pool));
> -      original_text = svn_stream_from_aprfile_range_readonly(f, FALSE,
> -                                                             start, end,
> -                                                             result_pool);
> -      svn_stream_set_line_filter_callback(original_text, original_line_filter);
> -      svn_stream_set_line_transformer_callback(original_text,
> -                                               remove_leading_char_transformer);
> +      original_text = stream_hunk_text_original(
> +                        svn_stream_from_aprfile_range_readonly(f, FALSE,
> +                                                               start, end,
> +                                                               result_pool),
> +                        result_pool);
>  
>        /* Create a stream which returns the modified hunk text. */
>        SVN_ERR(svn_io_file_open(&f, patch->path, flags, APR_OS_DEFAULT,
>                                 result_pool));
> -      modified_text = svn_stream_from_aprfile_range_readonly(f, FALSE,
> -                                                             start, end,
> -                                                             result_pool);
> -      svn_stream_set_line_filter_callback(modified_text, modified_line_filter);
> -      svn_stream_set_line_transformer_callback(modified_text,
> -                                               remove_leading_char_transformer);
> +      modified_text = stream_hunk_text_modified(
> +                        svn_stream_from_aprfile_range_readonly(f, FALSE,
> +                                                               start, end,
> +                                                               result_pool),
> +                        result_pool);
> +
>        /* Set the hunk's texts. */
>        (*hunk)->diff_text = diff_text;
>        if (reverse)
>          {
> -          svn_stream_set_line_transformer_callback(diff_text,
> -                                                   reverse_diff_transformer);
>            (*hunk)->original_text = modified_text;
>            (*hunk)->modified_text = original_text;
>          }
> @@ -918,7 +1232,7 @@ svn_diff_parse_next_patch(svn_patch_t **patch,
>        /* Remember the current line's offset, and read the line. */
>        last_line = pos;
>        SVN_ERR(svn_stream_readline_detect_eol(stream, &line, NULL, &eof,
> -                                             iterpool));
> +                                             iterpool, iterpool));
>  
>        if (! eof)
>          {
> Index: subversion/libsvn_subr/stream.c
> ===================================================================
> --- subversion/libsvn_subr/stream.c	(revision 961349)
> +++ subversion/libsvn_subr/stream.c	(working copy)
> @@ -53,8 +53,7 @@ struct svn_stream_t {
>    svn_io_reset_fn_t reset_fn;
>    svn_io_mark_fn_t mark_fn;
>    svn_io_seek_fn_t seek_fn;
> -  svn_io_line_filter_cb_t line_filter_cb;
> -  svn_io_line_transformer_cb_t line_transformer_cb;
> +  svn_io_readline_fn_t readline_fn;
>  };
>  
>  
> @@ -73,8 +72,7 @@ svn_stream_create(void *baton, apr_pool_t *pool)
>    stream->reset_fn = NULL;
>    stream->mark_fn = NULL;
>    stream->seek_fn = NULL;
> -  stream->line_filter_cb = NULL;
> -  stream->line_transformer_cb = NULL;
> +  stream->readline_fn = NULL;
>    return stream;
>  }
>  
> @@ -124,20 +122,11 @@ svn_stream_set_seek(svn_stream_t *stream, svn_io_s
>  }
>  
>  void
> -svn_stream_set_line_filter_callback(svn_stream_t *stream,
> -                                    svn_io_line_filter_cb_t line_filter_cb)
> +svn_stream_set_readline(svn_stream_t *stream, svn_io_readline_fn_t readline_fn)
>  {
> -  stream->line_filter_cb = line_filter_cb;
> +  stream->readline_fn = readline_fn;
>  }
>  
> -void
> -svn_stream_set_line_transformer_callback(
> -  svn_stream_t *stream,
> -  svn_io_line_transformer_cb_t line_transformer_cb)
> -{
> -  stream->line_transformer_cb = line_transformer_cb;
> -}
> -
>  svn_error_t *
>  svn_stream_read(svn_stream_t *stream, char *buffer, apr_size_t *len)
>  {
> @@ -233,55 +222,23 @@ svn_stream_printf_from_utf8(svn_stream_t *stream,
>    return svn_stream_write(stream, translated, &len);
>  }
>  
> -/* If a line filter callback was set on STREAM, invoke it on LINE,
> - * and indicate in *FILTERED whether the line should be filtered.
> - * If no line filter callback was set on STREAM, just set *FILTERED to FALSE.
> - */
> -static svn_error_t *
> -line_filter(svn_stream_t *stream, svn_boolean_t *filtered, const char *line,
> -            apr_pool_t *pool)
> -{
> -  apr_pool_t *scratch_pool;
> -
> -  if (! stream->line_filter_cb)
> -    {
> -      *filtered = FALSE;
> -      return SVN_NO_ERROR;
> -    }
> -
> -  scratch_pool = svn_pool_create(pool);
> -  SVN_ERR(stream->line_filter_cb(filtered, line, stream->baton, scratch_pool));
> -  svn_pool_destroy(scratch_pool);
> -  return SVN_NO_ERROR;
> -}
> -
> -/* Run the line transformer callback of STREAM with LINE as input,
> - * and expect the transformation result to be returned in BUF,
> - * allocated in POOL. */
> -static svn_error_t *
> -line_transformer(svn_stream_t *stream, svn_stringbuf_t **buf,
> -                 const char *line, apr_pool_t *pool, apr_pool_t *scratch_pool)
> -{
> -  *buf = NULL;  /* so we can assert that the callback has set it non-null */
> -  SVN_ERR(stream->line_transformer_cb(buf, line, stream->baton,
> -                                      pool, scratch_pool));
> -
> -  /* Die if the line transformer didn't provide any output. */
> -  SVN_ERR_ASSERT(*buf);
> -
> -  return SVN_NO_ERROR;
> -}
> -
> -/* Scan STREAM for an end-of-line indicatior, and return it in *EOL.
> +/* Invoke the READ_FN function of a stream to scan  for an end-of-line
> + * indicatior, and return it in *EOL.
> + * Use MARK_FN and SEEK_FN to seek in the stream.
>   * Set *EOL to NULL if the stream runs out before an end-of-line indicator
>   * is found. */
>  static svn_error_t *
> -scan_eol(const char **eol, svn_stream_t *stream, apr_pool_t *pool)
> +scan_eol(void *baton,
> +         svn_read_fn_t read_fn,
> +         svn_io_mark_fn_t mark_fn,
> +         svn_io_seek_fn_t seek_fn,
> +         const char **eol,
> +         apr_pool_t *pool)
>  {
>    const char *eol_str;
>    svn_stream_mark_t *mark;
>  
> -  SVN_ERR(svn_stream_mark(stream, &mark, pool));
> +  SVN_ERR(mark_fn(baton, &mark, pool));
>  
>    eol_str = NULL;
>    while (! eol_str)
> @@ -290,113 +247,82 @@ static svn_error_t *
>        apr_size_t len;
>  
>        len = sizeof(buf);
> -      SVN_ERR(svn_stream_read(stream, buf, &len));
> +      SVN_ERR(read_fn(baton, buf, &len));
>        if (len == 0)
>            break; /* EOF */
>        eol_str = svn_eol__detect_eol(buf, buf + len);
>      }
>  
> -  SVN_ERR(svn_stream_seek(stream, mark));
> +  SVN_ERR(seek_fn(baton, mark));
>  
>    *eol = eol_str;
>  
>    return SVN_NO_ERROR;
>  }
>  
> -/* Guts of svn_stream_readline() and svn_stream_readline_detect_eol().
> - * Returns the line read from STREAM in *STRINGBUF, and indicates
> - * end-of-file in *EOF.  If DETECT_EOL is TRUE, the end-of-line indicator
> - * is detected automatically and returned in *EOL.
> - * If DETECT_EOL is FALSE, *EOL must point to the desired end-of-line
> - * indicator.  STRINGBUF is allocated in POOL. */
> +/* An implementation of svn_io_readline_fn_t */
>  static svn_error_t *
> -stream_readline(svn_stringbuf_t **stringbuf,
> -                svn_boolean_t *eof,
> +stream_readline(void *baton,
> +                svn_read_fn_t read_fn,
> +                svn_io_mark_fn_t mark_fn,
> +                svn_io_seek_fn_t seek_fn,
> +                svn_stringbuf_t **stringbuf,
>                  const char **eol,
> -                svn_stream_t *stream,
> -                svn_boolean_t detect_eol,
> -                apr_pool_t *pool)
> +                svn_boolean_t *eof,
> +                apr_pool_t *result_pool,
> +                apr_pool_t *scratch_pool)
>  {
> -  svn_stringbuf_t *str;
> -  apr_pool_t *iterpool;
> -  svn_boolean_t filtered;
> +  apr_size_t numbytes;
> +  const char *match;
> +  char c;
>    const char *eol_str;
> +  /* Since we're reading one character at a time, let's at least
> +     optimize for the 90% case.  90% of the time, we can avoid the
> +     stringbuf ever having to realloc() itself if we start it out at
> +     80 chars.  */
> +  svn_stringbuf_t *str = svn_stringbuf_create_ensure(80, scratch_pool);
>  
> -  *eof = FALSE;
> -
> -  iterpool = svn_pool_create(pool);
> -  do
> +  if (eol == NULL || *eol == NULL)
>      {
> -      apr_size_t numbytes;
> -      const char *match;
> -      char c;
> -
> -      svn_pool_clear(iterpool);
> -
> -      /* Since we're reading one character at a time, let's at least
> -         optimize for the 90% case.  90% of the time, we can avoid the
> -         stringbuf ever having to realloc() itself if we start it out at
> -         80 chars.  */
> -      str = svn_stringbuf_create_ensure(80, iterpool);
> -
> -      if (detect_eol)
> +      SVN_ERR(scan_eol(baton, read_fn, mark_fn, seek_fn,
> +                       &eol_str, scratch_pool));
> +      if (! eol_str)
>          {
> -          SVN_ERR(scan_eol(&eol_str, stream, iterpool));
> -          if (eol)
> -            *eol = eol_str;
> -          if (! eol_str)
> -            {
> -              /* No newline until EOF, EOL_STR can be anything. */
> -              eol_str = APR_EOL_STR;
> -            }
> +          /* No newline until EOF, EOL_STR can be anything. */
> +          eol_str = APR_EOL_STR;
>          }
> -      else
> -        eol_str = *eol;
>  
> -      /* Read into STR up to and including the next EOL sequence. */
> -      match = eol_str;
> +      if (eol)
> +        *eol = eol_str;
> +    }
> +  else
> +    eol_str = *eol;
> +
> +  /* Read into STR up to and including the next EOL sequence. */
> +  match = eol_str;
> +  while (*match)
> +    {
>        numbytes = 1;
> -      while (*match)
> +      SVN_ERR(read_fn(baton, &c, &numbytes));
> +      if (numbytes != 1)
>          {
> -          SVN_ERR(svn_stream_read(stream, &c, &numbytes));
> -          if (numbytes != 1)
> -            {
> -              /* a 'short' read means the stream has run out. */
> -              *eof = TRUE;
> -              /* We know we don't have a whole EOL sequence, but ensure we
> -               * don't chop off any partial EOL sequence that we may have. */
> -              match = eol_str;
> -              /* Process this short (or empty) line just like any other
> -               * except with *EOF set. */
> -              break;
> -            }
> -
> -          if (c == *match)
> -            match++;
> -          else
> -            match = eol_str;
> -
> -          svn_stringbuf_appendbytes(str, &c, 1);
> +          /* a 'short' read means the stream has run out. */
> +          *eof = TRUE;
> +          *stringbuf = svn_stringbuf_dup(str, result_pool);
> +          return SVN_NO_ERROR;
>          }
>  
> -      svn_stringbuf_chop(str, match - eol_str);
> +      if (c == *match)
> +        match++;
> +      else
> +        match = eol_str;
>  
> -      SVN_ERR(line_filter(stream, &filtered, str->data, iterpool));
> +      svn_stringbuf_appendbytes(str, &c, 1);
>      }
> -  while (filtered && ! *eof);
> -  /* Not destroying the iterpool just yet since we still need STR
> -   * which is allocated in it. */
>  
> -  if (filtered)
> -    *stringbuf = svn_stringbuf_create_ensure(0, pool);
> -  else if (stream->line_transformer_cb)
> -    SVN_ERR(line_transformer(stream, stringbuf, str->data, pool, iterpool));
> -  else
> -    *stringbuf = svn_stringbuf_dup(str, pool);
> -
> -  /* Done. RIP iterpool. */
> -  svn_pool_destroy(iterpool);
> -
> +  *eof = FALSE;
> +  svn_stringbuf_chop(str, match - eol_str);
> +  *stringbuf = svn_stringbuf_dup(str, result_pool);
>    return SVN_NO_ERROR;
>  }
>  
> @@ -407,8 +333,25 @@ svn_stream_readline(svn_stream_t *stream,
>                      svn_boolean_t *eof,
>                      apr_pool_t *pool)
>  {
> -  return svn_error_return(stream_readline(stringbuf, eof, &eol, stream,
> -                                          FALSE, pool));
> +  svn_io_readline_fn_t readline_fn;
> +
> +  /* Provide a default readline implementation if the stream
> +   * hasn't overriden it. This is needed for backwards compat
> +   * to 1.6.x and earlier. */
> +  if (stream->readline_fn)
> +    readline_fn = stream->readline_fn;
> +  else
> +    readline_fn = stream_readline;
> +
> +  /* Caller must provide an EOL character. */
> +  SVN_ERR_ASSERT(eol && *eol);
> +
> +  return svn_error_return(readline_fn(stream->baton,
> +                                      stream->read_fn,
> +                                      stream->mark_fn,
> +                                      stream->seek_fn,
> +                                      stringbuf, &eol, eof,
> +                                      pool, pool));
>  }
>  
>  svn_error_t *
> @@ -416,13 +359,35 @@ svn_stream_readline_detect_eol(svn_stream_t *strea
>                                 svn_stringbuf_t **stringbuf,
>                                 const char **eol,
>                                 svn_boolean_t *eof,
> -                               apr_pool_t *pool)
> +                               apr_pool_t *result_pool,
> +                               apr_pool_t *scratch_pool)
>  {
> -  return svn_error_return(stream_readline(stringbuf, eof, eol, stream,
> -                                          TRUE, pool));
> -}
> +  svn_io_readline_fn_t readline_fn;
>  
> +  /* Provide a default readline implementation if the stream
> +   * hasn't overriden it. This is not needed for backwards compat
> +   * to 1.6.x and earlier (this function is new in 1.7), but it
> +   * is nice anyway because it saves us from adding dummy readline
> +   * methods to custom streams sprinkled throughout the code. */
> +  if (stream->readline_fn)
> +    readline_fn = stream->readline_fn;
> +  else
> +    readline_fn = stream_readline;
>  
> +  /* EOL-detection requires mark/seek support. */
> +  if (stream->mark_fn == NULL)
> +    return svn_error_create(SVN_ERR_STREAM_SEEK_NOT_SUPPORTED, NULL, NULL);
> +  if (stream->seek_fn == NULL)
> +    return svn_error_create(SVN_ERR_STREAM_SEEK_NOT_SUPPORTED, NULL, NULL);
> +
> +  return svn_error_return(readline_fn(stream->baton,
> +                                      stream->read_fn,
> +                                      stream->mark_fn,
> +                                      stream->seek_fn,
> +                                      stringbuf, eol, eof,
> +                                      result_pool, scratch_pool));
> +}
> +
>  svn_error_t *svn_stream_copy3(svn_stream_t *from, svn_stream_t *to,
>                                svn_cancel_func_t cancel_func,
>                                void *cancel_baton,
> @@ -536,7 +501,27 @@ seek_handler_empty(void *baton, svn_stream_mark_t
>    return SVN_NO_ERROR;
>  }
>  
> +static svn_error_t *
> +readline_handler_empty(void *baton,
> +                       svn_read_fn_t read_fn,
> +                       svn_io_mark_fn_t mark_fn,
> +                       svn_io_seek_fn_t seek_fn,
> +                       svn_stringbuf_t **stringbuf,
> +                       const char **eol,
> +                       svn_boolean_t *eof,
> +                       apr_pool_t *result_pool,
> +                       apr_pool_t *scratch_pool)
> +{
> +  *stringbuf = svn_stringbuf_create_ensure(0, result_pool);
>  
> +  if (eol && *eol == NULL)
> +    *eol = APR_EOL_STR;
> +
> +  *eof = TRUE;
> +
> +  return SVN_NO_ERROR;
> +}
> +
>  svn_stream_t *
>  svn_stream_empty(apr_pool_t *pool)
>  {
> @@ -548,6 +533,8 @@ svn_stream_empty(apr_pool_t *pool)
>    svn_stream_set_reset(stream, reset_handler_empty);
>    svn_stream_set_mark(stream, mark_handler_empty);
>    svn_stream_set_seek(stream, seek_handler_empty);
> +  svn_stream_set_readline(stream, readline_handler_empty);
> +
>    return stream;
>  }
>  
> @@ -652,6 +639,7 @@ svn_stream_disown(svn_stream_t *stream, apr_pool_t
>    svn_stream_set_reset(s, reset_handler_disown);
>    svn_stream_set_mark(s, mark_handler_disown);
>    svn_stream_set_seek(s, seek_handler_disown);
> +  svn_stream_set_readline(s, stream_readline);
>  
>    return s;
>  }
> @@ -820,6 +808,7 @@ svn_stream_from_aprfile2(apr_file_t *file,
>    svn_stream_set_reset(stream, reset_handler_apr);
>    svn_stream_set_mark(stream, mark_handler_apr);
>    svn_stream_set_seek(stream, seek_handler_apr);
> +  svn_stream_set_readline(stream, stream_readline);
>  
>    if (! disown)
>      svn_stream_set_close(stream, close_handler_apr);
> @@ -898,6 +887,7 @@ svn_stream_from_aprfile_range_readonly(apr_file_t
>    svn_stream_set_reset(stream, reset_handler_apr);
>    svn_stream_set_mark(stream, mark_handler_apr);
>    svn_stream_set_seek(stream, seek_handler_apr);
> +  svn_stream_set_readline(stream, stream_readline);
>  
>    if (! disown)
>      svn_stream_set_close(stream, close_handler_apr);
> @@ -1187,6 +1177,7 @@ svn_stream_compressed(svn_stream_t *stream, apr_po
>    svn_stream_set_read(zstream, read_handler_gz);
>    svn_stream_set_write(zstream, write_handler_gz);
>    svn_stream_set_close(zstream, close_handler_gz);
> +  svn_stream_set_readline(zstream, stream_readline);
>  
>    return zstream;
>  }
> @@ -1302,6 +1293,8 @@ svn_stream_checksummed2(svn_stream_t *stream,
>    svn_stream_set_read(s, read_handler_checksum);
>    svn_stream_set_write(s, write_handler_checksum);
>    svn_stream_set_close(s, close_handler_checksum);
> +  svn_stream_set_readline(s, stream_readline);
> +
>    return s;
>  }
>  
> @@ -1384,6 +1377,8 @@ svn_stream_checksummed(svn_stream_t *stream,
>    svn_stream_set_read(s, read_handler_md5);
>    svn_stream_set_write(s, write_handler_md5);
>    svn_stream_set_close(s, close_handler_md5);
> +  svn_stream_set_readline(s, stream_readline);
> +
>    return s;
>  }
>  
> @@ -1475,6 +1470,8 @@ svn_stream_from_stringbuf(svn_stringbuf_t *str,
>    svn_stream_set_reset(stream, reset_handler_stringbuf);
>    svn_stream_set_mark(stream, mark_handler_stringbuf);
>    svn_stream_set_seek(stream, seek_handler_stringbuf);
> +  svn_stream_set_readline(stream, stream_readline);
> +
>    return stream;
>  }
>  
> @@ -1511,6 +1508,8 @@ svn_stream_from_string(const svn_string_t *str,
>    baton->amt_read = 0;
>    stream = svn_stream_create(baton, pool);
>    svn_stream_set_read(stream, read_handler_string);
> +  svn_stream_set_readline(stream, stream_readline);
> +
>    return stream;
>  }
>  
> Index: subversion/libsvn_client/patch.c
> ===================================================================
> --- subversion/libsvn_client/patch.c	(revision 961349)
> +++ subversion/libsvn_client/patch.c	(working copy)
> @@ -530,9 +530,10 @@ read_line(patch_target_t *target,
>        APR_ARRAY_PUSH(target->lines, svn_stream_mark_t *) = mark;
>      }
>  
> +  eol_str = NULL;
>    SVN_ERR(svn_stream_readline_detect_eol(target->stream, &line_raw,
>                                           &eol_str, &target->eof,
> -                                         scratch_pool));
> +                                         scratch_pool, scratch_pool));
>    if (target->eol_style == svn_subst_eol_style_none)
>      target->eol_str = eol_str;
>  
> @@ -632,7 +633,8 @@ match_hunk(svn_boolean_t *matched, patch_target_t
>  
>        SVN_ERR(svn_stream_readline_detect_eol(hunk->original_text,
>                                               &hunk_line, NULL,
> -                                             &hunk_eof, iterpool));
> +                                             &hunk_eof,
> +                                             iterpool, iterpool));
>        /* Contract keywords, if any, before matching. */
>        SVN_ERR(svn_subst_translate_cstring2(hunk_line->data,
>                                             &hunk_line_translated,
> @@ -676,7 +678,8 @@ match_hunk(svn_boolean_t *matched, patch_target_t
>        /* If the target has no newline at end-of-file, we get an EOF
>         * indication for the target earlier than we do get it for the hunk. */
>        SVN_ERR(svn_stream_readline_detect_eol(hunk->original_text, &hunk_line,
> -                                             NULL, &hunk_eof, iterpool));
> +                                             NULL, &hunk_eof,
> +                                             iterpool, iterpool));
>        if (hunk_line->len == 0 && hunk_eof)
>          *matched = lines_matched;
>        else
> @@ -927,8 +930,10 @@ reject_hunk(patch_target_t *target, hunk_info_t *h
>  
>        svn_pool_clear(iterpool);
>  
> +      eol_str = NULL;
>        SVN_ERR(svn_stream_readline_detect_eol(hi->hunk->diff_text, &hunk_line,
> -                                             &eol_str, &eof, iterpool));
> +                                             &eol_str, &eof, iterpool,
> +                                             iterpool));
>        if (! eof)
>          {
>            if (hunk_line->len >= 1)
> @@ -992,9 +997,10 @@ apply_hunk(patch_target_t *target, hunk_info_t *hi
>  
>        svn_pool_clear(iterpool);
>  
> +      eol_str = NULL;
>        SVN_ERR(svn_stream_readline_detect_eol(hi->hunk->modified_text,
>                                               &hunk_line, &eol_str,
> -                                             &eof, iterpool));
> +                                             &eof, iterpool, iterpool));
>        lines_read++;
>        if (! eof && lines_read > hi->fuzz &&
>            lines_read <= hi->hunk->modified_length - hi->fuzz)
> Index: subversion/tests/libsvn_subr/stream-test.c
> ===================================================================
> --- subversion/tests/libsvn_subr/stream-test.c	(revision 961349)
> +++ subversion/tests/libsvn_subr/stream-test.c	(working copy)
> @@ -307,143 +307,7 @@ test_stream_range(apr_pool_t *pool)
>      return SVN_NO_ERROR;
>  }
>  
> -/* An implementation of svn_io_line_filter_cb_t */
>  static svn_error_t *
> -line_filter(svn_boolean_t *filtered, const char *line, void *baton,
> -            apr_pool_t *scratch_pool)
> -{
> -  *filtered = strchr(line, '!') != NULL;
> -  return SVN_NO_ERROR;
> -}
> -
> -static svn_error_t *
> -test_stream_line_filter(apr_pool_t *pool)
> -{
> -  static const char *lines[4] = {"Not filtered.", "Filtered!",
> -                                 "Not filtered either.", "End of the lines!"};
> -  svn_string_t *string;
> -  svn_stream_t *stream;
> -  svn_stringbuf_t *line;
> -  svn_boolean_t eof;
> -
> -  string = svn_string_createf(pool, "%s\n%s\n%s\n%s", lines[0], lines[1],
> -                              lines[2], lines[3]);
> -  stream = svn_stream_from_string(string, pool);
> -
> -  svn_stream_set_line_filter_callback(stream, line_filter);
> -
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_STRING_ASSERT(line->data, lines[0]);
> -  /* line[1] should be filtered */
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_STRING_ASSERT(line->data, lines[2]);
> -
> -  /* The last line should also be filtered, and the resulting
> -   * stringbuf should be empty. */
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_ASSERT(eof && svn_stringbuf_isempty(line));
> -
> -  return SVN_NO_ERROR;
> -}
> -
> -/* An implementation of svn_io_line_transformer_cb_t */
> -static svn_error_t *
> -line_transformer(svn_stringbuf_t **buf, const char *line, void *baton,
> -                 apr_pool_t *result_pool, apr_pool_t *scratch_pool)
> -{
> -  int i, len = strlen(line);
> -  char *temp = apr_palloc(scratch_pool, len + 1 );
> -
> -  for (i = 0; i < len; i++)
> -    {
> -      temp[i] = line[len - 1 - i];
> -    }
> -
> -  temp[len] = '\0';
> -
> -  *buf = svn_stringbuf_create(temp, result_pool);
> -
> -  return SVN_NO_ERROR;
> -}
> -
> -static svn_error_t *
> -test_stream_line_transformer(apr_pool_t *pool)
> -{
> -  static const char *lines[4] = {"gamma", "",
> -                                 "iota", "!"};
> -
> -  static const char *inv_lines[4] = {"ammag", "",
> -                                 "atoi", "!"};
> -  svn_string_t *string;
> -  svn_stream_t *stream;
> -  svn_stringbuf_t *line;
> -  svn_boolean_t eof;
> -
> -  string = svn_string_createf(pool, "%s\n%s\n%s\n%s", lines[0], lines[1],
> -                              lines[2], lines[3]);
> -
> -  stream = svn_stream_from_string(string, pool);
> -
> -  svn_stream_set_line_transformer_callback(stream, line_transformer);
> -
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_STRING_ASSERT(line->data, inv_lines[0]);
> -
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_STRING_ASSERT(line->data, inv_lines[1]);
> -
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_STRING_ASSERT(line->data, inv_lines[2]);
> -
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_STRING_ASSERT(line->data, inv_lines[3]);
> -
> -  /* We should have reached eof and the stringbuf should be emtpy. */
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_ASSERT(eof && svn_stringbuf_isempty(line));
> -
> -  return SVN_NO_ERROR;
> -}
> -
> -static svn_error_t *
> -test_stream_line_filter_and_transformer(apr_pool_t *pool)
> -{
> -  static const char *lines[4] = {"!gamma", "",
> -                                 "iota", "!"};
> -
> -  static const char *inv_lines[4] = {"ammag", "",
> -                                 "atoi", "!"};
> -  svn_string_t *string;
> -  svn_stream_t *stream;
> -  svn_stringbuf_t *line;
> -  svn_boolean_t eof;
> -
> -  string = svn_string_createf(pool, "%s\n%s\n%s\n%s", lines[0], lines[1],
> -                              lines[2], lines[3]);
> -
> -  stream = svn_stream_from_string(string, pool);
> -
> -  svn_stream_set_line_filter_callback(stream, line_filter);
> -
> -  svn_stream_set_line_transformer_callback(stream, line_transformer);
> -
> -  /* Line one should be filtered. */
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_STRING_ASSERT(line->data, inv_lines[1]);
> -
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_STRING_ASSERT(line->data, inv_lines[2]);
> -
> -  /* The last line should also be filtered, and the resulting
> -   * stringbuf should be empty. */
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_ASSERT(eof && svn_stringbuf_isempty(line));
> -
> -  return SVN_NO_ERROR;
> -
> -}
> -
> -static svn_error_t *
>  test_stream_tee(apr_pool_t *pool)
>  {
>    svn_stringbuf_t *test_bytes = generate_test_bytes(100, pool);
> @@ -647,12 +511,6 @@ struct svn_test_descriptor_t test_funcs[] =
>                     "test compressed streams"),
>      SVN_TEST_PASS2(test_stream_range,
>                     "test streams reading from range of file"),
> -    SVN_TEST_PASS2(test_stream_line_filter,
> -                   "test stream line filtering"),
> -    SVN_TEST_PASS2(test_stream_line_transformer,
> -                   "test stream line transforming"),
> -    SVN_TEST_PASS2(test_stream_line_filter_and_transformer,
> -                   "test stream line filtering and transforming"),
>      SVN_TEST_PASS2(test_stream_tee,
>                     "test 'tee' streams"),
>      SVN_TEST_PASS2(test_stream_seek_file,



Re: [PATCH] issue #3555: make svn_stream_readline() a stream method

Posted by Julian Foad <ju...@wandisco.com>.
Hi Stefan.  I've just started looking at this.  Overall impression is it
looks like you've done it very well - thank you!

Could you possibly get the readline_detect_eol() API changes out of this
patch, and make them in a separate patch either before or after this?
Or if that change belongs in this patch, please explain why in the log
msg.

Thanks.
- Julian


On Wed, 2010-07-07 at 20:39 +0200, Stefan Sperling wrote:
> This change was proposed by Julian some time ago, because having
> callback types that change the behaviour of svn_stream_readline(),
> namely svn_io_line_filter_cb_t and svn_io_line_transformer_cb_t,
> was the wrong approach.
> See http://subversion.tigris.org/issues/show_bug.cgi?id=3555
> 
> The patch below implements Julian's second suggestion from the issue:
> "make readline() a virtual method of svn_stream_t and then have the user
> provide replacement implementations of readline() for doing the required
> filtering."
> 
> One caveat is that to remain backwards compatible we cannot require
> streams to set a readline method. So svn_stream_readline() falls back
> to the default readline implementation if none has been set.
> 
> I have taken a shortcut and made svn_stream_readline_detect_eol() (which
> is new in 1.7) also provide a readline fallback. This avoids having to
> add dummy readline methods to streams elsewhere in our code, such as the
> one in fs_fs.c.
> 
> svn_stream_readline_detect_eol() now behaves like svn_stream_readline()
> if an EOL character is passed in.
> I briefly considered deprecating svn_stream_readline() and renaming
> svn_stream_readline_detect_eol() to svn_stream_readline2(), but
> decided against doing so as it would cause a lot of code churn and
> existing callers of svn_stream_readline() won't benefit from the upgrade.
> 
> The readline callbacks were initially added to address the needs of svn
> patch, in particularly the diff parser. The parser now has two custom readline
> implementations. There is some code duplication between implementations.
> But the generic readline method becomes a bit simpler again. It's not as
> simple as it was in 1.6.x, because we need a way to read lines with arbitrary
> EOL characters. The diffstat illustrates nicely how code has shifted around:
> 
> 
>  include/svn_io.h                |   97 +++-----
>  libsvn_client/patch.c           |   16 -
>  libsvn_diff/parse-diff.c        |  452 +++++++++++++++++++++++++++++++++-------
>  libsvn_subr/stream.c            |  285 ++++++++++++-------------
>  tests/libsvn_subr/stream-test.c |  142 ------------
>  5 files changed, 572 insertions(+), 420 deletions(-)
> 
> The patch passes make check.
> I'm posting it here for review by Julian and others who are interested.
> 
> Thanks,
> Stefan
> 
> [[[
> Fix issue #3555, 'Remove the "line_filter" and "line_transformer" callbacks
> from svn_stream_t'.
> 
> Make svn_stream_readline() a virtual method of svn_stream_t,
> and provide custom implementations of readline methods in the diff
> parsing code (which is the only place where we need this right now).
> 
> * subversion/include/svn_io.h
>   (svn_io_line_filter_cb_t, svn_io_line_transformer_cb_t,
>    svn_stream_set_line_filter_callback,
>    svn_stream_set_line_transformer_callback): Remove.
>   (svn_io_readline_fn_t, svn_stream_set_readline): Declare.
>   (svn_stream_readline): Adjust docstring.
>   (svn_stream_readline_detect_eol): Switch to dual-pool paradigm,
>    and document the new twisted behaviour of the EOL input/output parameter.
> 
> * subversion/libsvn_diff/parse-diff.c
>   (original_line_filter, modified_line_filter, remove_leading_char_transformer,
>    reverse_diff_transformer): Remove.
>   (hunk_text_stream_baton, read_handler_hunk_text, write_handler_hunk_text,
>    close_handler_hunk_text, reset_handler_hunk_text, mark_handler_hunk_text,
>    seek_handler_hunk_text, scan_eol, readline_handler_hunk_text,
>    stream_hunk_text, stream_hunk_text_original, stream_hunk_text_modified):
>     New. Implementation of a stream which provides a special readline
>     method to read original/modified texts of hunks from a patch file stream.
>   (reverse_diff_text_stream_baton, read_handler_reverse_diff_text,
>    write_handler_reverse_diff_text, close_handler_reverse_diff_text,
>    reset_handler_reverse_diff_text, mark_handler_reverse_diff_text,
>    seek_handler_reverse_diff_text, readline_handler_reverse_diff_text,
>    stream_reverse_diff_text): New. Implementation of a stream which
>     provides a special readline method which reverses unidiff data read
>     from the wrapped stream.
>   (parse_next_hunk): Track svn_stream_readline_detect_eol() dual-pool change.
>    Add a comment explaining why the patch file gets opened multiple
>    times (drive-by fix because this confused me at first).
>    Instead of installing line-filter/transformation callbacks on
>    streams, wrap streams with appropriate wrapper streams.
> 
> * subversion/libsvn_subr/stream.c
>   (struct svn_stream_t): Replace line_filter_cb and line_transformer_cb
>    members with readline_fn member.
>   (svn_stream_create): Track changes to svn_stream_t.
>   (svn_stream_set_line_filter_callback,
>    svn_stream_set_line_transformer_callback,
>    line_filter, line_transformer): Remove.
>   (scan_eol): Tweak argument list for use within a stream method.
>    This function can no longer expect a stream, so pass a baton
>    and a set of required stream methods instead.
>   (stream_readline): Make this in svn_io_readline_fn_t implementation.
>    Remove handling of line filters/transformers.
>   (svn_stream_readline): Instead of calling the stream_readline() helper
>    directly, call a custom readline implementation if one is set on the
>    stream. If no custom implementation is provided, fall back to the
>    stream_readline() helper function to preserve compatibility with 1.6.x.
>    Assert that the caller passed an EOL character, because the new
>    svn_io_readline_fn_t does not strictly require it.
>   (svn_stream_readline_detect_eol): As previous, but ensure that
>    the stream has mark/seek support instead of asserting an EOL.
>   (readline_handler_empty): Custom readline handler for the empty stream.
>   (svn_stream_empty, svn_stream_disown, svn_stream_from_aprfile2,
>    svn_stream_from_aprfile_range_readonly, svn_stream_compressed,
>    svn_stream_checksummed2, svn_stream_checksummed, svn_stream_from_stringbuf,
>    svn_stream_from_string): Set a readline method.
> 
> * subversion/libsvn_client/patch.c
>   (read_line, match_hunk, reject_hunk, apply_hunk): Ensure that EOL_STR
>     is always initialised to fulfill new API requirements of
>     svn_stream_readline_detect_eol regarding EOLs.
>     Also pass two pools to svn_stream_readline_detect_eol().
> 
> * subversion/tests/libsvn_subr/stream-test.c
>   (line_filter, test_stream_line_filter, line_transformer,
>    test_stream_line_transformer,
>    test_stream_line_filter_and_transformer): Remove these tests.
>   (test_funcs): Remove removed tests.
> 
> ]]]
> 
> 
> Index: subversion/include/svn_io.h
> ===================================================================
> --- subversion/include/svn_io.h	(revision 961349)
> +++ subversion/include/svn_io.h	(working copy)
> @@ -780,40 +780,36 @@ typedef svn_error_t *(*svn_io_mark_fn_t)(void *bat
>  typedef svn_error_t *(*svn_io_seek_fn_t)(void *baton,
>                                           svn_stream_mark_t *mark);
>  
> -/** Line-filtering callback function for a generic stream.
> - * @a baton is the stream's baton.
> - * @see svn_stream_t, svn_stream_set_baton() and svn_stream_readline().
> +/** Line-reading handler function for a generic stream.
>   *
> - * @since New in 1.7.
> - */
> -typedef svn_error_t *(*svn_io_line_filter_cb_t)(svn_boolean_t *filtered,
> -                                                const char *line,
> -                                                void *baton,
> -                                                apr_pool_t *scratch_pool);
> -
> -/** A callback function, invoked by svn_stream_readline(), which can perform
> - * arbitary transformations on the line before it is passed back to the caller
> - * of svn_stream_readline().
> + * Allocate @a *stringbuf in @a result_pool, and read into it one line
> + * from @a stream. If @a *eol is not NULL, it is used as the expected
> + * line terminator. If @a eol is NULL, the line-terminator is detected
> + * automatically. If @a *eol is NULL, the line-terminator is detected
> + * automatically and is returned in @a *eol.
>   *
> - * Returns a transformed stringbuf in @a buf, allocated in @a result_pool.
> - * This callback gets invoked on lines which were not filtered by the
> - * line-filtering callback function.
> + * @a mark_fn and @a seek_fn are required to be non-NULL if the end-of-line
> + * indicator is to be detected automatically. Else, they may be NULL.
>   *
> - * Implementations should always at least return an empty stringbuf.
> - * It is a fatal error if an implementation returns @a *buf as NULL.
> + * The line-terminator is read from the stream, but is not added to
> + * the end of the stringbuf.  Instead, the stringbuf ends with a usual '\\0'.
>   *
> - * @a baton is the stream's baton.
> + * If @a stream runs out of bytes before encountering a line-terminator,
> + * then set @a *eof to @c TRUE, otherwise set @a *eof to FALSE.
>   *
> - * @see svn_stream_t, svn_stream_set_baton(), svn_io_line_filter_cb_t and
> - * svn_stream_readline().
> + * Temporary allocations will be performed in @a scratch_pool.
>   *
> - * @since New in 1.7.
> - */
> -typedef svn_error_t *(*svn_io_line_transformer_cb_t)(svn_stringbuf_t **buf,
> -                                                     const char *line,
> -                                                     void *baton,
> -                                                     apr_pool_t *result_pool,
> -                                                     apr_pool_t *scratch_pool);
> + * @see svn_stream_readline and svn_stream_readline_detect_eol.
> + * @since New in 1.7. */
> +typedef svn_error_t *(*svn_io_readline_fn_t)(void *baton,
> +                                             svn_read_fn_t read_fn,
> +                                             svn_io_mark_fn_t mark_fn,
> +                                             svn_io_seek_fn_t seek_fn,
> +                                             svn_stringbuf_t **stringbuf,
> +                                             const char **eol,
> +                                             svn_boolean_t *eof,
> +                                             apr_pool_t *result_pool,
> +                                             apr_pool_t *scratch_pool);
>  
>  /** Create a generic stream.  @see svn_stream_t. */
>  svn_stream_t *
> @@ -864,23 +860,11 @@ void
>  svn_stream_set_seek(svn_stream_t *stream,
>                      svn_io_seek_fn_t seek_fn);
>  
> -/** Set @a stream's line-filtering callback function to @a line_filter_cb
> - *
> - * @since New in 1.7.
> - */
> +/** Set @a stream's readline function to @a readline_fn */
>  void
> -svn_stream_set_line_filter_callback(svn_stream_t *stream,
> -                                    svn_io_line_filter_cb_t line_filter_cb);
> +svn_stream_set_readline(svn_stream_t *stream,
> +                        svn_io_readline_fn_t readline_fn);
>  
> -/** Set @a streams's line-transforming callback function to
> - * @a line_transformer_cb.
> - *
> - * @since New in 1.7.
> - */
> -void
> -svn_stream_set_line_transformer_callback(
> -  svn_stream_t *stream,
> -  svn_io_line_transformer_cb_t line_transformer_cb);
>  
>  /** Create a stream that is empty for reading and infinite for writing. */
>  svn_stream_t *
> @@ -1203,19 +1187,6 @@ svn_stream_printf_from_utf8(svn_stream_t *stream,
>   *
>   * If @a stream runs out of bytes before encountering a line-terminator,
>   * then set @a *eof to @c TRUE, otherwise set @a *eof to FALSE.
> - *
> - * If a line-filter callback function was set on the stream using
> - * svn_stream_set_line_filter_callback(), lines will only be returned
> - * if they pass the filtering decision of the callback function.
> - * If end-of-file is encountered while reading the line and the line
> - * is filtered, @a *stringbuf will be empty.
> - *
> - * If a line-transformer callback function was set on the stream using
> - * svn_stream_set_line_transformer_callback(), lines will be returned
> - * transformed, in a way determined by the callback.
> - *
> - * Note that the line-transformer callback gets called after the line-filter
> - * callback, not before.
>   */
>  svn_error_t *
>  svn_stream_readline(svn_stream_t *stream,
> @@ -1225,11 +1196,14 @@ svn_stream_readline(svn_stream_t *stream,
>                      apr_pool_t *pool);
>  
>  /**
> - * Similar to svn_stream_readline(). The line-terminator is detected
> - * automatically.  If @a eol is not NULL, the detected line-terminator
> - * is returned in @a *eol.  If EOF is reached and the stream does not
> - * end with a newline character, @a *eol will be NULL.
> + * Similar to svn_stream_readline(). If @a *eol is not NULL, it is used
> + * as the expected line terminator. If @a eol is NULL, the line-terminator
> + * is detected automatically. If @a *eol is NULL, the line-terminator is
> + * detected automatically and is returned in @a *eol.
>   *
> + * The @a *stringbuf will be allocated in @a result_pool.
> + * @a scratch_pool is used for temporary allocations.
> + * 
>   * @since New in 1.7.
>   */
>  svn_error_t *
> @@ -1237,7 +1211,8 @@ svn_stream_readline_detect_eol(svn_stream_t *strea
>                                 svn_stringbuf_t **stringbuf,
>                                 const char **eol,
>                                 svn_boolean_t *eof,
> -                               apr_pool_t *pool);
> +                               apr_pool_t *result_pool,
> +                               apr_pool_t *scratch_pool);
>  
>  /**
>   * Read the contents of the readable stream @a from and write them to the
> Index: subversion/libsvn_diff/parse-diff.c
> ===================================================================
> --- subversion/libsvn_diff/parse-diff.c	(revision 961349)
> +++ subversion/libsvn_diff/parse-diff.c	(working copy)
> @@ -33,6 +33,8 @@
>  #include "svn_dirent_uri.h"
>  #include "svn_diff.h"
>  
> +#include "private/svn_eol_private.h"
> +
>  /* Helper macro for readability */
>  #define starts_with(str, start)  \
>    (strncmp((str), (start), strlen(start)) == 0)
> @@ -184,89 +186,398 @@ parse_hunk_header(const char *header, svn_hunk_t *
>    return TRUE;
>  }
>  
> -/* A stream line-filter which allows only original text from a hunk,
> - * and filters special lines (which start with a backslash). */
> +/* Users of the diff parsing API are provided with various streams
> + * to read lines from a hunk. These streams return:
> + *
> + *  - the original hunk text (all lines starting with '-' or ' ')
> + *  - the modified hunk text (all lines starting with '+' or ' ')
> + *  - the plain unidiff text (possibly reversed)
> + *
> + * To achieve this, we wrap the patch file stream with custom streams,
> + * which override the wrapped stream's readline method. */
> +
> +/* Baton for a stream that reads hunk texts. */
> +struct hunk_text_stream_baton {
> +  /* The leading unidiff symbol of lines which should be filtered.
> +   * Can be '+' or '-', depending on whether we're providing the original
> +   * or modified version of the hunk. */
> +  char verboten;
> +
> +  svn_stream_t *wrapped_stream;
> +};
> +
> +/* An implementation of svn_read_fn_t. */
>  static svn_error_t *
> -original_line_filter(svn_boolean_t *filtered, const char *line, void *baton,
> -                     apr_pool_t *scratch_pool)
> +read_handler_hunk_text(void *baton, char *buffer, apr_size_t *len)
>  {
> -  *filtered = (line[0] == '+' || line[0] == '\\');
> -  return SVN_NO_ERROR;
> +  struct hunk_text_stream_baton *b = baton;
> +  return svn_stream_read(b->wrapped_stream, buffer, len);
>  }
>  
> -/* A stream line-filter which allows only modified text from a hunk,
> - * and filters special lines (which start with a backslash). */
> +/* An implementation of svn_write_fn_t. */
>  static svn_error_t *
> -modified_line_filter(svn_boolean_t *filtered, const char *line, void *baton,
> -                     apr_pool_t *scratch_pool)
> +write_handler_hunk_text(void *baton, const char *buffer, apr_size_t *len)
>  {
> -  *filtered = (line[0] == '-' || line[0] == '\\');
> -  return SVN_NO_ERROR;
> +  struct hunk_text_stream_baton *b = baton;
> +  return svn_stream_write(b->wrapped_stream, buffer, len);
>  }
>  
> -/** line-transformer callback to shave leading diff symbols. */
> +/* An implementation of svn_close_fn_t. */
>  static svn_error_t *
> -remove_leading_char_transformer(svn_stringbuf_t **buf,
> -                                const char *line,
> -                                void *baton,
> -                                apr_pool_t *result_pool,
> -                                apr_pool_t *scratch_pool)
> +close_handler_hunk_text(void *baton)
>  {
> -  if (line[0] == '+' || line[0] == '-' || line[0] == ' ')
> -    *buf = svn_stringbuf_create(line + 1, result_pool);
> -  else
> -    *buf = svn_stringbuf_create(line, result_pool);
> +  struct hunk_text_stream_baton *b = baton;
> +  return svn_stream_close(b->wrapped_stream);
> +}
>  
> +/* An implementation of svn_io_reset_fn_t. */
> +static svn_error_t *
> +reset_handler_hunk_text(void *baton)
> +{
> +  struct hunk_text_stream_baton *b = baton;
> +  return svn_stream_reset(b->wrapped_stream);
> +}
> +
> +/* An implementation of svn_io_mark_fn_t. */
> +static svn_error_t *
> +mark_handler_hunk_text(void *baton, svn_stream_mark_t **mark, apr_pool_t *pool)
> +{
> +  struct hunk_text_stream_baton *b = baton;
> +  return svn_stream_mark(b->wrapped_stream, mark, pool);
> +}
> +
> +/* An implementation of svn_io_seek_fn_t. */
> +static svn_error_t *
> +seek_handler_hunk_text(void *baton, svn_stream_mark_t *mark)
> +{
> +  struct hunk_text_stream_baton *b = baton;
> +  return svn_stream_seek(b->wrapped_stream, mark);
> +}
> +
> +/* Invoke the READ_FN function of a stream to scan for an end-of-line
> + * indicator, and return it in *EOL.
> + * Set *EOL to NULL if the stream runs out before an end-of-line indicator
> + * is found. */
> +static svn_error_t *
> +scan_eol(void *baton,
> +         svn_read_fn_t read_fn,
> +         svn_io_mark_fn_t mark_fn,
> +         svn_io_seek_fn_t seek_fn,
> +         const char **eol,
> +         apr_pool_t *pool)
> +{
> +  const char *eol_str;
> +  svn_stream_mark_t *mark;
> +
> +  SVN_ERR(mark_fn(baton, &mark, pool));
> +
> +  eol_str = NULL;
> +  while (! eol_str)
> +    {
> +      char buf[512];
> +      apr_size_t len;
> +
> +      len = sizeof(buf);
> +      SVN_ERR(read_fn(baton, buf, &len));
> +      if (len == 0)
> +          break; /* EOF */
> +      eol_str = svn_eol__detect_eol(buf, buf + len);
> +    }
> +
> +  SVN_ERR(seek_fn(baton, mark));
> +
> +  *eol = eol_str;
> +
>    return SVN_NO_ERROR;
>  }
>  
> -/** line-transformer callback to reverse a diff text. */
> +/* An implementation of svn_io_readline_fn_t. */
>  static svn_error_t *
> -reverse_diff_transformer(svn_stringbuf_t **buf,
> -                         const char *line,
> -                         void *baton,
> -                         apr_pool_t *result_pool,
> -                         apr_pool_t *scratch_pool)
> +readline_handler_hunk_text(void *baton,
> +                           svn_read_fn_t read_fn,
> +                           svn_io_mark_fn_t mark_fn,
> +                           svn_io_seek_fn_t seek_fn,
> +                           svn_stringbuf_t **stringbuf,
> +                           const char **eol,
> +                           svn_boolean_t *eof,
> +                           apr_pool_t *result_pool,
> +                           apr_pool_t *scratch_pool)
>  {
> -  svn_hunk_t hunk;
> +  svn_stringbuf_t *str;
> +  apr_pool_t *iterpool;
> +  svn_boolean_t filtered;
> +  const char *eol_str;
> +  struct hunk_text_stream_baton *b = baton;
>  
> -  /* ### Pass the already parsed hunk via the baton?
> -   * ### Maybe we should really make svn_stream_readline() a proper stream
> -   * ### method and override it instead of adding special callbacks? */
> -  if (parse_hunk_header(line, &hunk, "@@", FALSE, scratch_pool))
> +  *eof = FALSE;
> +
> +  iterpool = svn_pool_create(scratch_pool);
> +  do
>      {
> -      *buf = svn_stringbuf_createf(result_pool,
> -                                   "@@ -%lu,%lu +%lu,%lu @@",
> -                                   hunk.modified_start,
> -                                   hunk.modified_length,
> -                                   hunk.original_start,
> -                                   hunk.original_length);
> +      apr_size_t numbytes;
> +      const char *match;
> +      char c;
> +
> +      svn_pool_clear(iterpool);
> +
> +      /* Since we're reading one character at a time, let's at least
> +         optimize for the 90% case.  90% of the time, we can avoid the
> +         stringbuf ever having to realloc() itself if we start it out at
> +         80 chars.  */
> +      str = svn_stringbuf_create_ensure(80, iterpool);
> +
> +      if (eol == NULL || *eol == NULL)
> +        {
> +          SVN_ERR(scan_eol(baton, read_fn, mark_fn, seek_fn,
> +                           &eol_str, iterpool));
> +          if (! eol_str)
> +            {
> +              /* No newline until EOF, EOL_STR can be anything. */
> +              eol_str = APR_EOL_STR;
> +            }
> +          if (eol)
> +            *eol = eol_str;
> +        }
> +      else
> +        eol_str = *eol;
> +
> +      /* Read into STR up to and including the next EOL sequence. */
> +      match = eol_str;
> +      numbytes = 1;
> +      while (*match)
> +        {
> +          SVN_ERR(read_fn(baton, &c, &numbytes));
> +          if (numbytes != 1)
> +            {
> +              /* a 'short' read means the stream has run out. */
> +              *eof = TRUE;
> +              /* We know we don't have a whole EOL sequence, but ensure we
> +               * don't chop off any partial EOL sequence that we may have. */
> +              match = eol_str;
> +              /* Process this short (or empty) line just like any other
> +               * except with *EOF set. */
> +              break;
> +            }
> +
> +          if (c == *match)
> +            match++;
> +          else
> +            match = eol_str;
> +
> +          svn_stringbuf_appendbytes(str, &c, 1);
> +        }
> +
> +      svn_stringbuf_chop(str, match - eol_str);
> +      filtered = (str->data[0] == b->verboten || str->data[0] == '\\');
>      }
> -  else if (parse_hunk_header(line, &hunk, "##", FALSE, scratch_pool))
> +  while (filtered && ! *eof);
> +  /* Not destroying the iterpool just yet since we still need STR
> +   * which is allocated in it. */
> +
> +  if (filtered)
>      {
> -      *buf = svn_stringbuf_createf(result_pool,
> -                                   "## -%lu,%lu +%lu,%lu ##",
> -                                   hunk.modified_start,
> -                                   hunk.modified_length,
> -                                   hunk.original_start,
> -                                   hunk.original_length);
> +      /* EOF, return an empty string. */
> +      *stringbuf = svn_stringbuf_create_ensure(0, result_pool);
>      }
> -  else if (line[0] == '+')
> +  else if (str->data[0] == '+' || str->data[0] == '-' || str->data[0] == ' ')
>      {
> -      *buf = svn_stringbuf_create(line, result_pool);
> -      (*buf)->data[0] = '-';
> +      /* Shave off leading unidiff symbols. */
> +      *stringbuf = svn_stringbuf_create(str->data + 1, result_pool);
>      }
> -  else if (line[0] == '-')
> +  else
>      {
> -      *buf = svn_stringbuf_create(line, result_pool);
> -      (*buf)->data[0] = '+';
> +      /* Return the line as-is. */
> +      *stringbuf = svn_stringbuf_dup(str, result_pool);
>      }
> +
> +  /* Done. RIP iterpool. */
> +  svn_pool_destroy(iterpool);
> +
> +  return SVN_NO_ERROR;
> +}
> +
> +/* Return a stream for reading hunk text from WRAPPED_STREAM.
> + * VERBOTEN is the leading character of lines which should be
> + * filtered by this stream's readline method ('+' or '-').
> + * Allocate the new stream in RESULT_POOL. */
> +static svn_stream_t *
> +stream_hunk_text(svn_stream_t *wrapped_stream,
> +                 char verboten,
> +                 apr_pool_t *result_pool)
> +{
> +  svn_stream_t *stream;
> +  struct hunk_text_stream_baton *baton;
> +
> +  baton = apr_palloc(result_pool, sizeof(*baton));
> +  baton->wrapped_stream = wrapped_stream;
> +  baton->verboten = verboten;
> +
> +  stream = svn_stream_create(baton, result_pool);
> +
> +  svn_stream_set_read(stream, read_handler_hunk_text);
> +  svn_stream_set_write(stream, write_handler_hunk_text);
> +  svn_stream_set_close(stream, close_handler_hunk_text);
> +  svn_stream_set_reset(stream, reset_handler_hunk_text);
> +  svn_stream_set_mark(stream, mark_handler_hunk_text);
> +  svn_stream_set_seek(stream, seek_handler_hunk_text);
> +  svn_stream_set_readline(stream, readline_handler_hunk_text);
> +
> +  return stream;
> +}
> +
> +/* Return a stream to read the original text of a hunk from
> + * WRAPPED_STREAM, and allocated in RESULT_POOL.*/
> +static svn_stream_t *
> +stream_hunk_text_original(svn_stream_t *wrapped_stream,
> +                          apr_pool_t *result_pool)
> +{
> +  return stream_hunk_text(wrapped_stream, '+', result_pool);
> +}
> +
> +/* Return a stream to read the modified text of a hunk from
> + * WRAPPED_STREAM, and allocated in RESULT_POOL.*/
> +static svn_stream_t *
> +stream_hunk_text_modified(svn_stream_t *wrapped_stream,
> +                          apr_pool_t *result_pool)
> +{
> +  return stream_hunk_text(wrapped_stream, '-', result_pool);
> +}
> +
> +
> +/* Baton for a stream that reads unidiff text reversed. */
> +struct reverse_diff_text_stream_baton {
> +  svn_stream_t *wrapped_stream;
> +}; 
> +
> +/* An implementation of svn_read_fn_t. */
> +static svn_error_t *
> +read_handler_reverse_diff_text(void *baton, char *buffer, apr_size_t *len)
> +{
> +  struct reverse_diff_text_stream_baton *b = baton;
> +  return svn_stream_read(b->wrapped_stream, buffer, len);
> +}
> +
> +/* An implementation of svn_write_fn_t. */
> +static svn_error_t *
> +write_handler_reverse_diff_text(void *baton, const char *buffer,
> +                                apr_size_t *len)
> +{
> +  struct reverse_diff_text_stream_baton *b = baton;
> +  return svn_stream_write(b->wrapped_stream, buffer, len);
> +}
> +
> +/* An implementation of svn_close_fn_t. */
> +static svn_error_t *
> +close_handler_reverse_diff_text(void *baton)
> +{
> +  struct reverse_diff_text_stream_baton *b = baton;
> +  return svn_stream_close(b->wrapped_stream);
> +}
> +
> +/* An implementation of svn_io_reset_fn_t. */
> +static svn_error_t *
> +reset_handler_reverse_diff_text(void *baton)
> +{
> +  struct reverse_diff_text_stream_baton *b = baton;
> +  return svn_stream_reset(b->wrapped_stream);
> +}
> +
> +/* An implementation of svn_io_mark_fn_t. */
> +static svn_error_t *
> +mark_handler_reverse_diff_text(void *baton, svn_stream_mark_t **mark,
> +                               apr_pool_t *pool)
> +{
> +  struct reverse_diff_text_stream_baton *b = baton;
> +  return svn_stream_mark(b->wrapped_stream, mark, pool);
> +}
> +
> +/* An implementation of svn_io_seek_fn_t. */
> +static svn_error_t *
> +seek_handler_reverse_diff_text(void *baton, svn_stream_mark_t *mark)
> +{
> +  struct hunk_text_stream_baton *b = baton;
> +  return svn_stream_seek(b->wrapped_stream, mark);
> +}
> +
> +/* An implementation of svn_io_readline_fn_t. */
> +static svn_error_t *
> +readline_handler_reverse_diff_text(void *baton,
> +                                   svn_read_fn_t read_fn,
> +                                   svn_io_mark_fn_t mark_fn,
> +                                   svn_io_seek_fn_t seek_fn,
> +                                   svn_stringbuf_t **stringbuf,
> +                                   const char **eol,
> +                                   svn_boolean_t *eof,
> +                                   apr_pool_t *result_pool,
> +                                   apr_pool_t *scratch_pool)
> +{
> +  svn_hunk_t hunk;
> +  svn_stringbuf_t *line;
> +  struct reverse_diff_text_stream_baton *b = baton;
> +
> +  /* Read the line and perform necessary transformations to
> +   * produce a reversed diff. */
> +  SVN_ERR(svn_stream_readline_detect_eol(b->wrapped_stream,
> +                                         &line, eol, eof,
> +                                         result_pool, scratch_pool));
> +  if (parse_hunk_header(line->data, &hunk, "@@", FALSE, scratch_pool))
> +    {
> +      /* Line is a hunk header, reverse it. */
> +      *stringbuf = svn_stringbuf_createf(result_pool,
> +                                         "@@ -%lu,%lu +%lu,%lu @@",
> +                                         hunk.modified_start,
> +                                         hunk.modified_length,
> +                                         hunk.original_start,
> +                                         hunk.original_length);
> +    }
> +  else if (parse_hunk_header(line->data, &hunk, "##", FALSE, scratch_pool))
> +    {
> +      /* Line is a hunk header, reverse it. */
> +      *stringbuf = svn_stringbuf_createf(result_pool,
> +                                         "## -%lu,%lu +%lu,%lu ##",
> +                                         hunk.modified_start,
> +                                         hunk.modified_length,
> +                                         hunk.original_start,
> +                                         hunk.original_length);
> +    }
>    else
> -    *buf = svn_stringbuf_create(line, result_pool);
> +    {
> +      if (line->data[0] == '+')
> +        line->data[0] = '-';
> +      else if (line->data[0] == '-')
> +        line->data[0] = '+';
>  
> +      *stringbuf = line;
> +    }
> +
>    return SVN_NO_ERROR;
>  }
>  
> +/* Return a stream for reading diff text from WRAPPED_STREAM.
> + * The unidiff will appear reversed when read via the stream's readline method.
> + * Allocate the new stream in RESULT_POOL. */
> +static svn_stream_t *
> +stream_reverse_diff_text(svn_stream_t *wrapped_stream,
> +                         apr_pool_t *result_pool)
> +{
> +  svn_stream_t *stream;
> +  struct reverse_diff_text_stream_baton *baton;
> +
> +  baton = apr_palloc(result_pool, sizeof(*baton));
> +  baton->wrapped_stream = wrapped_stream;
> +
> +  stream = svn_stream_create(baton, result_pool);
> +  svn_stream_set_read(stream, read_handler_reverse_diff_text);
> +  svn_stream_set_write(stream, write_handler_reverse_diff_text);
> +  svn_stream_set_close(stream, close_handler_reverse_diff_text);
> +  svn_stream_set_reset(stream, reset_handler_reverse_diff_text);
> +  svn_stream_set_mark(stream, mark_handler_reverse_diff_text);
> +  svn_stream_set_seek(stream, seek_handler_reverse_diff_text);
> +  svn_stream_set_readline(stream, readline_handler_reverse_diff_text);
> +
> +  return stream;
> +}
> +
>  /* Parse PROP_NAME from HEADER as the part after the INDICATOR line. */
>  static svn_error_t *
>  parse_prop_name(const char **prop_name, const char *header, 
> @@ -349,7 +660,7 @@ parse_next_hunk(svn_hunk_t **hunk,
>        /* Remember the current line's offset, and read the line. */
>        last_line = pos;
>        SVN_ERR(svn_stream_readline_detect_eol(stream, &line, NULL, &eof,
> -                                             iterpool));
> +                                             iterpool, iterpool));
>  
>        if (! eof)
>          {
> @@ -502,38 +813,41 @@ parse_next_hunk(svn_hunk_t **hunk,
>        apr_file_t *f;
>        apr_int32_t flags = APR_READ | APR_BUFFERED;
>  
> +      /* For each of the streams created below, we re-open the patch file.
> +       * Each stream needs its own file descriptor in order to have
> +       * independent seek behaviour. */
> +
>        /* Create a stream which returns the hunk text itself. */
>        SVN_ERR(svn_io_file_open(&f, patch->path, flags, APR_OS_DEFAULT,
>                                 result_pool));
>        diff_text = svn_stream_from_aprfile_range_readonly(f, FALSE,
>                                                           start, end,
>                                                           result_pool);
> +      if (reverse)
> +        diff_text = stream_reverse_diff_text(diff_text, result_pool);
>  
>        /* Create a stream which returns the original hunk text. */
>        SVN_ERR(svn_io_file_open(&f, patch->path, flags, APR_OS_DEFAULT,
>                                 result_pool));
> -      original_text = svn_stream_from_aprfile_range_readonly(f, FALSE,
> -                                                             start, end,
> -                                                             result_pool);
> -      svn_stream_set_line_filter_callback(original_text, original_line_filter);
> -      svn_stream_set_line_transformer_callback(original_text,
> -                                               remove_leading_char_transformer);
> +      original_text = stream_hunk_text_original(
> +                        svn_stream_from_aprfile_range_readonly(f, FALSE,
> +                                                               start, end,
> +                                                               result_pool),
> +                        result_pool);
>  
>        /* Create a stream which returns the modified hunk text. */
>        SVN_ERR(svn_io_file_open(&f, patch->path, flags, APR_OS_DEFAULT,
>                                 result_pool));
> -      modified_text = svn_stream_from_aprfile_range_readonly(f, FALSE,
> -                                                             start, end,
> -                                                             result_pool);
> -      svn_stream_set_line_filter_callback(modified_text, modified_line_filter);
> -      svn_stream_set_line_transformer_callback(modified_text,
> -                                               remove_leading_char_transformer);
> +      modified_text = stream_hunk_text_modified(
> +                        svn_stream_from_aprfile_range_readonly(f, FALSE,
> +                                                               start, end,
> +                                                               result_pool),
> +                        result_pool);
> +
>        /* Set the hunk's texts. */
>        (*hunk)->diff_text = diff_text;
>        if (reverse)
>          {
> -          svn_stream_set_line_transformer_callback(diff_text,
> -                                                   reverse_diff_transformer);
>            (*hunk)->original_text = modified_text;
>            (*hunk)->modified_text = original_text;
>          }
> @@ -918,7 +1232,7 @@ svn_diff_parse_next_patch(svn_patch_t **patch,
>        /* Remember the current line's offset, and read the line. */
>        last_line = pos;
>        SVN_ERR(svn_stream_readline_detect_eol(stream, &line, NULL, &eof,
> -                                             iterpool));
> +                                             iterpool, iterpool));
>  
>        if (! eof)
>          {
> Index: subversion/libsvn_subr/stream.c
> ===================================================================
> --- subversion/libsvn_subr/stream.c	(revision 961349)
> +++ subversion/libsvn_subr/stream.c	(working copy)
> @@ -53,8 +53,7 @@ struct svn_stream_t {
>    svn_io_reset_fn_t reset_fn;
>    svn_io_mark_fn_t mark_fn;
>    svn_io_seek_fn_t seek_fn;
> -  svn_io_line_filter_cb_t line_filter_cb;
> -  svn_io_line_transformer_cb_t line_transformer_cb;
> +  svn_io_readline_fn_t readline_fn;
>  };
>  
>  
> @@ -73,8 +72,7 @@ svn_stream_create(void *baton, apr_pool_t *pool)
>    stream->reset_fn = NULL;
>    stream->mark_fn = NULL;
>    stream->seek_fn = NULL;
> -  stream->line_filter_cb = NULL;
> -  stream->line_transformer_cb = NULL;
> +  stream->readline_fn = NULL;
>    return stream;
>  }
>  
> @@ -124,20 +122,11 @@ svn_stream_set_seek(svn_stream_t *stream, svn_io_s
>  }
>  
>  void
> -svn_stream_set_line_filter_callback(svn_stream_t *stream,
> -                                    svn_io_line_filter_cb_t line_filter_cb)
> +svn_stream_set_readline(svn_stream_t *stream, svn_io_readline_fn_t readline_fn)
>  {
> -  stream->line_filter_cb = line_filter_cb;
> +  stream->readline_fn = readline_fn;
>  }
>  
> -void
> -svn_stream_set_line_transformer_callback(
> -  svn_stream_t *stream,
> -  svn_io_line_transformer_cb_t line_transformer_cb)
> -{
> -  stream->line_transformer_cb = line_transformer_cb;
> -}
> -
>  svn_error_t *
>  svn_stream_read(svn_stream_t *stream, char *buffer, apr_size_t *len)
>  {
> @@ -233,55 +222,23 @@ svn_stream_printf_from_utf8(svn_stream_t *stream,
>    return svn_stream_write(stream, translated, &len);
>  }
>  
> -/* If a line filter callback was set on STREAM, invoke it on LINE,
> - * and indicate in *FILTERED whether the line should be filtered.
> - * If no line filter callback was set on STREAM, just set *FILTERED to FALSE.
> - */
> -static svn_error_t *
> -line_filter(svn_stream_t *stream, svn_boolean_t *filtered, const char *line,
> -            apr_pool_t *pool)
> -{
> -  apr_pool_t *scratch_pool;
> -
> -  if (! stream->line_filter_cb)
> -    {
> -      *filtered = FALSE;
> -      return SVN_NO_ERROR;
> -    }
> -
> -  scratch_pool = svn_pool_create(pool);
> -  SVN_ERR(stream->line_filter_cb(filtered, line, stream->baton, scratch_pool));
> -  svn_pool_destroy(scratch_pool);
> -  return SVN_NO_ERROR;
> -}
> -
> -/* Run the line transformer callback of STREAM with LINE as input,
> - * and expect the transformation result to be returned in BUF,
> - * allocated in POOL. */
> -static svn_error_t *
> -line_transformer(svn_stream_t *stream, svn_stringbuf_t **buf,
> -                 const char *line, apr_pool_t *pool, apr_pool_t *scratch_pool)
> -{
> -  *buf = NULL;  /* so we can assert that the callback has set it non-null */
> -  SVN_ERR(stream->line_transformer_cb(buf, line, stream->baton,
> -                                      pool, scratch_pool));
> -
> -  /* Die if the line transformer didn't provide any output. */
> -  SVN_ERR_ASSERT(*buf);
> -
> -  return SVN_NO_ERROR;
> -}
> -
> -/* Scan STREAM for an end-of-line indicatior, and return it in *EOL.
> +/* Invoke the READ_FN function of a stream to scan  for an end-of-line
> + * indicatior, and return it in *EOL.
> + * Use MARK_FN and SEEK_FN to seek in the stream.
>   * Set *EOL to NULL if the stream runs out before an end-of-line indicator
>   * is found. */
>  static svn_error_t *
> -scan_eol(const char **eol, svn_stream_t *stream, apr_pool_t *pool)
> +scan_eol(void *baton,
> +         svn_read_fn_t read_fn,
> +         svn_io_mark_fn_t mark_fn,
> +         svn_io_seek_fn_t seek_fn,
> +         const char **eol,
> +         apr_pool_t *pool)
>  {
>    const char *eol_str;
>    svn_stream_mark_t *mark;
>  
> -  SVN_ERR(svn_stream_mark(stream, &mark, pool));
> +  SVN_ERR(mark_fn(baton, &mark, pool));
>  
>    eol_str = NULL;
>    while (! eol_str)
> @@ -290,113 +247,82 @@ static svn_error_t *
>        apr_size_t len;
>  
>        len = sizeof(buf);
> -      SVN_ERR(svn_stream_read(stream, buf, &len));
> +      SVN_ERR(read_fn(baton, buf, &len));
>        if (len == 0)
>            break; /* EOF */
>        eol_str = svn_eol__detect_eol(buf, buf + len);
>      }
>  
> -  SVN_ERR(svn_stream_seek(stream, mark));
> +  SVN_ERR(seek_fn(baton, mark));
>  
>    *eol = eol_str;
>  
>    return SVN_NO_ERROR;
>  }
>  
> -/* Guts of svn_stream_readline() and svn_stream_readline_detect_eol().
> - * Returns the line read from STREAM in *STRINGBUF, and indicates
> - * end-of-file in *EOF.  If DETECT_EOL is TRUE, the end-of-line indicator
> - * is detected automatically and returned in *EOL.
> - * If DETECT_EOL is FALSE, *EOL must point to the desired end-of-line
> - * indicator.  STRINGBUF is allocated in POOL. */
> +/* An implementation of svn_io_readline_fn_t */
>  static svn_error_t *
> -stream_readline(svn_stringbuf_t **stringbuf,
> -                svn_boolean_t *eof,
> +stream_readline(void *baton,
> +                svn_read_fn_t read_fn,
> +                svn_io_mark_fn_t mark_fn,
> +                svn_io_seek_fn_t seek_fn,
> +                svn_stringbuf_t **stringbuf,
>                  const char **eol,
> -                svn_stream_t *stream,
> -                svn_boolean_t detect_eol,
> -                apr_pool_t *pool)
> +                svn_boolean_t *eof,
> +                apr_pool_t *result_pool,
> +                apr_pool_t *scratch_pool)
>  {
> -  svn_stringbuf_t *str;
> -  apr_pool_t *iterpool;
> -  svn_boolean_t filtered;
> +  apr_size_t numbytes;
> +  const char *match;
> +  char c;
>    const char *eol_str;
> +  /* Since we're reading one character at a time, let's at least
> +     optimize for the 90% case.  90% of the time, we can avoid the
> +     stringbuf ever having to realloc() itself if we start it out at
> +     80 chars.  */
> +  svn_stringbuf_t *str = svn_stringbuf_create_ensure(80, scratch_pool);
>  
> -  *eof = FALSE;
> -
> -  iterpool = svn_pool_create(pool);
> -  do
> +  if (eol == NULL || *eol == NULL)
>      {
> -      apr_size_t numbytes;
> -      const char *match;
> -      char c;
> -
> -      svn_pool_clear(iterpool);
> -
> -      /* Since we're reading one character at a time, let's at least
> -         optimize for the 90% case.  90% of the time, we can avoid the
> -         stringbuf ever having to realloc() itself if we start it out at
> -         80 chars.  */
> -      str = svn_stringbuf_create_ensure(80, iterpool);
> -
> -      if (detect_eol)
> +      SVN_ERR(scan_eol(baton, read_fn, mark_fn, seek_fn,
> +                       &eol_str, scratch_pool));
> +      if (! eol_str)
>          {
> -          SVN_ERR(scan_eol(&eol_str, stream, iterpool));
> -          if (eol)
> -            *eol = eol_str;
> -          if (! eol_str)
> -            {
> -              /* No newline until EOF, EOL_STR can be anything. */
> -              eol_str = APR_EOL_STR;
> -            }
> +          /* No newline until EOF, EOL_STR can be anything. */
> +          eol_str = APR_EOL_STR;
>          }
> -      else
> -        eol_str = *eol;
>  
> -      /* Read into STR up to and including the next EOL sequence. */
> -      match = eol_str;
> +      if (eol)
> +        *eol = eol_str;
> +    }
> +  else
> +    eol_str = *eol;
> +
> +  /* Read into STR up to and including the next EOL sequence. */
> +  match = eol_str;
> +  while (*match)
> +    {
>        numbytes = 1;
> -      while (*match)
> +      SVN_ERR(read_fn(baton, &c, &numbytes));
> +      if (numbytes != 1)
>          {
> -          SVN_ERR(svn_stream_read(stream, &c, &numbytes));
> -          if (numbytes != 1)
> -            {
> -              /* a 'short' read means the stream has run out. */
> -              *eof = TRUE;
> -              /* We know we don't have a whole EOL sequence, but ensure we
> -               * don't chop off any partial EOL sequence that we may have. */
> -              match = eol_str;
> -              /* Process this short (or empty) line just like any other
> -               * except with *EOF set. */
> -              break;
> -            }
> -
> -          if (c == *match)
> -            match++;
> -          else
> -            match = eol_str;
> -
> -          svn_stringbuf_appendbytes(str, &c, 1);
> +          /* a 'short' read means the stream has run out. */
> +          *eof = TRUE;
> +          *stringbuf = svn_stringbuf_dup(str, result_pool);
> +          return SVN_NO_ERROR;
>          }
>  
> -      svn_stringbuf_chop(str, match - eol_str);
> +      if (c == *match)
> +        match++;
> +      else
> +        match = eol_str;
>  
> -      SVN_ERR(line_filter(stream, &filtered, str->data, iterpool));
> +      svn_stringbuf_appendbytes(str, &c, 1);
>      }
> -  while (filtered && ! *eof);
> -  /* Not destroying the iterpool just yet since we still need STR
> -   * which is allocated in it. */
>  
> -  if (filtered)
> -    *stringbuf = svn_stringbuf_create_ensure(0, pool);
> -  else if (stream->line_transformer_cb)
> -    SVN_ERR(line_transformer(stream, stringbuf, str->data, pool, iterpool));
> -  else
> -    *stringbuf = svn_stringbuf_dup(str, pool);
> -
> -  /* Done. RIP iterpool. */
> -  svn_pool_destroy(iterpool);
> -
> +  *eof = FALSE;
> +  svn_stringbuf_chop(str, match - eol_str);
> +  *stringbuf = svn_stringbuf_dup(str, result_pool);
>    return SVN_NO_ERROR;
>  }
>  
> @@ -407,8 +333,25 @@ svn_stream_readline(svn_stream_t *stream,
>                      svn_boolean_t *eof,
>                      apr_pool_t *pool)
>  {
> -  return svn_error_return(stream_readline(stringbuf, eof, &eol, stream,
> -                                          FALSE, pool));
> +  svn_io_readline_fn_t readline_fn;
> +
> +  /* Provide a default readline implementation if the stream
> +   * hasn't overriden it. This is needed for backwards compat
> +   * to 1.6.x and earlier. */
> +  if (stream->readline_fn)
> +    readline_fn = stream->readline_fn;
> +  else
> +    readline_fn = stream_readline;
> +
> +  /* Caller must provide an EOL character. */
> +  SVN_ERR_ASSERT(eol && *eol);
> +
> +  return svn_error_return(readline_fn(stream->baton,
> +                                      stream->read_fn,
> +                                      stream->mark_fn,
> +                                      stream->seek_fn,
> +                                      stringbuf, &eol, eof,
> +                                      pool, pool));
>  }
>  
>  svn_error_t *
> @@ -416,13 +359,35 @@ svn_stream_readline_detect_eol(svn_stream_t *strea
>                                 svn_stringbuf_t **stringbuf,
>                                 const char **eol,
>                                 svn_boolean_t *eof,
> -                               apr_pool_t *pool)
> +                               apr_pool_t *result_pool,
> +                               apr_pool_t *scratch_pool)
>  {
> -  return svn_error_return(stream_readline(stringbuf, eof, eol, stream,
> -                                          TRUE, pool));
> -}
> +  svn_io_readline_fn_t readline_fn;
>  
> +  /* Provide a default readline implementation if the stream
> +   * hasn't overriden it. This is not needed for backwards compat
> +   * to 1.6.x and earlier (this function is new in 1.7), but it
> +   * is nice anyway because it saves us from adding dummy readline
> +   * methods to custom streams sprinkled throughout the code. */
> +  if (stream->readline_fn)
> +    readline_fn = stream->readline_fn;
> +  else
> +    readline_fn = stream_readline;
>  
> +  /* EOL-detection requires mark/seek support. */
> +  if (stream->mark_fn == NULL)
> +    return svn_error_create(SVN_ERR_STREAM_SEEK_NOT_SUPPORTED, NULL, NULL);
> +  if (stream->seek_fn == NULL)
> +    return svn_error_create(SVN_ERR_STREAM_SEEK_NOT_SUPPORTED, NULL, NULL);
> +
> +  return svn_error_return(readline_fn(stream->baton,
> +                                      stream->read_fn,
> +                                      stream->mark_fn,
> +                                      stream->seek_fn,
> +                                      stringbuf, eol, eof,
> +                                      result_pool, scratch_pool));
> +}
> +
>  svn_error_t *svn_stream_copy3(svn_stream_t *from, svn_stream_t *to,
>                                svn_cancel_func_t cancel_func,
>                                void *cancel_baton,
> @@ -536,7 +501,27 @@ seek_handler_empty(void *baton, svn_stream_mark_t
>    return SVN_NO_ERROR;
>  }
>  
> +static svn_error_t *
> +readline_handler_empty(void *baton,
> +                       svn_read_fn_t read_fn,
> +                       svn_io_mark_fn_t mark_fn,
> +                       svn_io_seek_fn_t seek_fn,
> +                       svn_stringbuf_t **stringbuf,
> +                       const char **eol,
> +                       svn_boolean_t *eof,
> +                       apr_pool_t *result_pool,
> +                       apr_pool_t *scratch_pool)
> +{
> +  *stringbuf = svn_stringbuf_create_ensure(0, result_pool);
>  
> +  if (eol && *eol == NULL)
> +    *eol = APR_EOL_STR;
> +
> +  *eof = TRUE;
> +
> +  return SVN_NO_ERROR;
> +}
> +
>  svn_stream_t *
>  svn_stream_empty(apr_pool_t *pool)
>  {
> @@ -548,6 +533,8 @@ svn_stream_empty(apr_pool_t *pool)
>    svn_stream_set_reset(stream, reset_handler_empty);
>    svn_stream_set_mark(stream, mark_handler_empty);
>    svn_stream_set_seek(stream, seek_handler_empty);
> +  svn_stream_set_readline(stream, readline_handler_empty);
> +
>    return stream;
>  }
>  
> @@ -652,6 +639,7 @@ svn_stream_disown(svn_stream_t *stream, apr_pool_t
>    svn_stream_set_reset(s, reset_handler_disown);
>    svn_stream_set_mark(s, mark_handler_disown);
>    svn_stream_set_seek(s, seek_handler_disown);
> +  svn_stream_set_readline(s, stream_readline);
>  
>    return s;
>  }
> @@ -820,6 +808,7 @@ svn_stream_from_aprfile2(apr_file_t *file,
>    svn_stream_set_reset(stream, reset_handler_apr);
>    svn_stream_set_mark(stream, mark_handler_apr);
>    svn_stream_set_seek(stream, seek_handler_apr);
> +  svn_stream_set_readline(stream, stream_readline);
>  
>    if (! disown)
>      svn_stream_set_close(stream, close_handler_apr);
> @@ -898,6 +887,7 @@ svn_stream_from_aprfile_range_readonly(apr_file_t
>    svn_stream_set_reset(stream, reset_handler_apr);
>    svn_stream_set_mark(stream, mark_handler_apr);
>    svn_stream_set_seek(stream, seek_handler_apr);
> +  svn_stream_set_readline(stream, stream_readline);
>  
>    if (! disown)
>      svn_stream_set_close(stream, close_handler_apr);
> @@ -1187,6 +1177,7 @@ svn_stream_compressed(svn_stream_t *stream, apr_po
>    svn_stream_set_read(zstream, read_handler_gz);
>    svn_stream_set_write(zstream, write_handler_gz);
>    svn_stream_set_close(zstream, close_handler_gz);
> +  svn_stream_set_readline(zstream, stream_readline);
>  
>    return zstream;
>  }
> @@ -1302,6 +1293,8 @@ svn_stream_checksummed2(svn_stream_t *stream,
>    svn_stream_set_read(s, read_handler_checksum);
>    svn_stream_set_write(s, write_handler_checksum);
>    svn_stream_set_close(s, close_handler_checksum);
> +  svn_stream_set_readline(s, stream_readline);
> +
>    return s;
>  }
>  
> @@ -1384,6 +1377,8 @@ svn_stream_checksummed(svn_stream_t *stream,
>    svn_stream_set_read(s, read_handler_md5);
>    svn_stream_set_write(s, write_handler_md5);
>    svn_stream_set_close(s, close_handler_md5);
> +  svn_stream_set_readline(s, stream_readline);
> +
>    return s;
>  }
>  
> @@ -1475,6 +1470,8 @@ svn_stream_from_stringbuf(svn_stringbuf_t *str,
>    svn_stream_set_reset(stream, reset_handler_stringbuf);
>    svn_stream_set_mark(stream, mark_handler_stringbuf);
>    svn_stream_set_seek(stream, seek_handler_stringbuf);
> +  svn_stream_set_readline(stream, stream_readline);
> +
>    return stream;
>  }
>  
> @@ -1511,6 +1508,8 @@ svn_stream_from_string(const svn_string_t *str,
>    baton->amt_read = 0;
>    stream = svn_stream_create(baton, pool);
>    svn_stream_set_read(stream, read_handler_string);
> +  svn_stream_set_readline(stream, stream_readline);
> +
>    return stream;
>  }
>  
> Index: subversion/libsvn_client/patch.c
> ===================================================================
> --- subversion/libsvn_client/patch.c	(revision 961349)
> +++ subversion/libsvn_client/patch.c	(working copy)
> @@ -530,9 +530,10 @@ read_line(patch_target_t *target,
>        APR_ARRAY_PUSH(target->lines, svn_stream_mark_t *) = mark;
>      }
>  
> +  eol_str = NULL;
>    SVN_ERR(svn_stream_readline_detect_eol(target->stream, &line_raw,
>                                           &eol_str, &target->eof,
> -                                         scratch_pool));
> +                                         scratch_pool, scratch_pool));
>    if (target->eol_style == svn_subst_eol_style_none)
>      target->eol_str = eol_str;
>  
> @@ -632,7 +633,8 @@ match_hunk(svn_boolean_t *matched, patch_target_t
>  
>        SVN_ERR(svn_stream_readline_detect_eol(hunk->original_text,
>                                               &hunk_line, NULL,
> -                                             &hunk_eof, iterpool));
> +                                             &hunk_eof,
> +                                             iterpool, iterpool));
>        /* Contract keywords, if any, before matching. */
>        SVN_ERR(svn_subst_translate_cstring2(hunk_line->data,
>                                             &hunk_line_translated,
> @@ -676,7 +678,8 @@ match_hunk(svn_boolean_t *matched, patch_target_t
>        /* If the target has no newline at end-of-file, we get an EOF
>         * indication for the target earlier than we do get it for the hunk. */
>        SVN_ERR(svn_stream_readline_detect_eol(hunk->original_text, &hunk_line,
> -                                             NULL, &hunk_eof, iterpool));
> +                                             NULL, &hunk_eof,
> +                                             iterpool, iterpool));
>        if (hunk_line->len == 0 && hunk_eof)
>          *matched = lines_matched;
>        else
> @@ -927,8 +930,10 @@ reject_hunk(patch_target_t *target, hunk_info_t *h
>  
>        svn_pool_clear(iterpool);
>  
> +      eol_str = NULL;
>        SVN_ERR(svn_stream_readline_detect_eol(hi->hunk->diff_text, &hunk_line,
> -                                             &eol_str, &eof, iterpool));
> +                                             &eol_str, &eof, iterpool,
> +                                             iterpool));
>        if (! eof)
>          {
>            if (hunk_line->len >= 1)
> @@ -992,9 +997,10 @@ apply_hunk(patch_target_t *target, hunk_info_t *hi
>  
>        svn_pool_clear(iterpool);
>  
> +      eol_str = NULL;
>        SVN_ERR(svn_stream_readline_detect_eol(hi->hunk->modified_text,
>                                               &hunk_line, &eol_str,
> -                                             &eof, iterpool));
> +                                             &eof, iterpool, iterpool));
>        lines_read++;
>        if (! eof && lines_read > hi->fuzz &&
>            lines_read <= hi->hunk->modified_length - hi->fuzz)
> Index: subversion/tests/libsvn_subr/stream-test.c
> ===================================================================
> --- subversion/tests/libsvn_subr/stream-test.c	(revision 961349)
> +++ subversion/tests/libsvn_subr/stream-test.c	(working copy)
> @@ -307,143 +307,7 @@ test_stream_range(apr_pool_t *pool)
>      return SVN_NO_ERROR;
>  }
>  
> -/* An implementation of svn_io_line_filter_cb_t */
>  static svn_error_t *
> -line_filter(svn_boolean_t *filtered, const char *line, void *baton,
> -            apr_pool_t *scratch_pool)
> -{
> -  *filtered = strchr(line, '!') != NULL;
> -  return SVN_NO_ERROR;
> -}
> -
> -static svn_error_t *
> -test_stream_line_filter(apr_pool_t *pool)
> -{
> -  static const char *lines[4] = {"Not filtered.", "Filtered!",
> -                                 "Not filtered either.", "End of the lines!"};
> -  svn_string_t *string;
> -  svn_stream_t *stream;
> -  svn_stringbuf_t *line;
> -  svn_boolean_t eof;
> -
> -  string = svn_string_createf(pool, "%s\n%s\n%s\n%s", lines[0], lines[1],
> -                              lines[2], lines[3]);
> -  stream = svn_stream_from_string(string, pool);
> -
> -  svn_stream_set_line_filter_callback(stream, line_filter);
> -
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_STRING_ASSERT(line->data, lines[0]);
> -  /* line[1] should be filtered */
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_STRING_ASSERT(line->data, lines[2]);
> -
> -  /* The last line should also be filtered, and the resulting
> -   * stringbuf should be empty. */
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_ASSERT(eof && svn_stringbuf_isempty(line));
> -
> -  return SVN_NO_ERROR;
> -}
> -
> -/* An implementation of svn_io_line_transformer_cb_t */
> -static svn_error_t *
> -line_transformer(svn_stringbuf_t **buf, const char *line, void *baton,
> -                 apr_pool_t *result_pool, apr_pool_t *scratch_pool)
> -{
> -  int i, len = strlen(line);
> -  char *temp = apr_palloc(scratch_pool, len + 1 );
> -
> -  for (i = 0; i < len; i++)
> -    {
> -      temp[i] = line[len - 1 - i];
> -    }
> -
> -  temp[len] = '\0';
> -
> -  *buf = svn_stringbuf_create(temp, result_pool);
> -
> -  return SVN_NO_ERROR;
> -}
> -
> -static svn_error_t *
> -test_stream_line_transformer(apr_pool_t *pool)
> -{
> -  static const char *lines[4] = {"gamma", "",
> -                                 "iota", "!"};
> -
> -  static const char *inv_lines[4] = {"ammag", "",
> -                                 "atoi", "!"};
> -  svn_string_t *string;
> -  svn_stream_t *stream;
> -  svn_stringbuf_t *line;
> -  svn_boolean_t eof;
> -
> -  string = svn_string_createf(pool, "%s\n%s\n%s\n%s", lines[0], lines[1],
> -                              lines[2], lines[3]);
> -
> -  stream = svn_stream_from_string(string, pool);
> -
> -  svn_stream_set_line_transformer_callback(stream, line_transformer);
> -
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_STRING_ASSERT(line->data, inv_lines[0]);
> -
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_STRING_ASSERT(line->data, inv_lines[1]);
> -
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_STRING_ASSERT(line->data, inv_lines[2]);
> -
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_STRING_ASSERT(line->data, inv_lines[3]);
> -
> -  /* We should have reached eof and the stringbuf should be emtpy. */
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_ASSERT(eof && svn_stringbuf_isempty(line));
> -
> -  return SVN_NO_ERROR;
> -}
> -
> -static svn_error_t *
> -test_stream_line_filter_and_transformer(apr_pool_t *pool)
> -{
> -  static const char *lines[4] = {"!gamma", "",
> -                                 "iota", "!"};
> -
> -  static const char *inv_lines[4] = {"ammag", "",
> -                                 "atoi", "!"};
> -  svn_string_t *string;
> -  svn_stream_t *stream;
> -  svn_stringbuf_t *line;
> -  svn_boolean_t eof;
> -
> -  string = svn_string_createf(pool, "%s\n%s\n%s\n%s", lines[0], lines[1],
> -                              lines[2], lines[3]);
> -
> -  stream = svn_stream_from_string(string, pool);
> -
> -  svn_stream_set_line_filter_callback(stream, line_filter);
> -
> -  svn_stream_set_line_transformer_callback(stream, line_transformer);
> -
> -  /* Line one should be filtered. */
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_STRING_ASSERT(line->data, inv_lines[1]);
> -
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_STRING_ASSERT(line->data, inv_lines[2]);
> -
> -  /* The last line should also be filtered, and the resulting
> -   * stringbuf should be empty. */
> -  svn_stream_readline(stream, &line, "\n", &eof, pool);
> -  SVN_TEST_ASSERT(eof && svn_stringbuf_isempty(line));
> -
> -  return SVN_NO_ERROR;
> -
> -}
> -
> -static svn_error_t *
>  test_stream_tee(apr_pool_t *pool)
>  {
>    svn_stringbuf_t *test_bytes = generate_test_bytes(100, pool);
> @@ -647,12 +511,6 @@ struct svn_test_descriptor_t test_funcs[] =
>                     "test compressed streams"),
>      SVN_TEST_PASS2(test_stream_range,
>                     "test streams reading from range of file"),
> -    SVN_TEST_PASS2(test_stream_line_filter,
> -                   "test stream line filtering"),
> -    SVN_TEST_PASS2(test_stream_line_transformer,
> -                   "test stream line transforming"),
> -    SVN_TEST_PASS2(test_stream_line_filter_and_transformer,
> -                   "test stream line filtering and transforming"),
>      SVN_TEST_PASS2(test_stream_tee,
>                     "test 'tee' streams"),
>      SVN_TEST_PASS2(test_stream_seek_file,