<commit_before>/*
 * main.c: Subversion dump stream filtering tool.
 *
 * ====================================================================
 * Copyright (c) 2000-2004 CollabNet.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://subversion.tigris.org/license-1.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 *
 * This software consists of voluntary contributions made by many
 * individuals.  For exact contribution history, see the revision
 * history and logs, available at http://subversion.tigris.org/.
 * ====================================================================
 */


#include <stdlib.h>

#include <apr_file_io.h>

#include "svn_private_config.h"
#include "svn_cmdline.h"
#include "svn_error.h"
#include "svn_opt.h"
#include "svn_utf.h"
#include "svn_path.h"
#include "svn_hash.h"
#include "svn_repos.h"
#include "svn_pools.h"
#include "svn_sorts.h"
#include "svn_props.h"



/*** Code. ***/

/* Helper to open stdio streams */

/* NOTE: we used to call svn_stream_from_stdio(), which wraps a stream
   around a standard stdio.h FILE pointer.  The problem is that these
   pointers operate through C Run Time (CRT) on Win32, which does all
   sorts of translation on them: LF's become CRLF's, and ctrl-Z's
   embedded in Word documents are interpreted as premature EOF's.

   So instead, we use apr_file_open_std*, which bypass the CRT and
   directly wrap the OS's file-handles, which don't know or care about
   translation.  Thus dump/load works correctly on Win32.
*/
static svn_error_t *
create_stdio_stream (svn_stream_t **stream,
                     APR_DECLARE(apr_status_t) open_fn (apr_file_t **,
                                                        apr_pool_t *),
                     apr_pool_t *pool)
{
  apr_file_t *stdio_file;
  apr_status_t apr_err = open_fn (&stdio_file, pool);

  if (apr_err)
    return svn_error_wrap_apr (apr_err, _("Can't open stdio file"));

  *stream = svn_stream_from_aprfile (stdio_file, pool);
  return SVN_NO_ERROR;
}


/* Writes a property in dumpfile format to given stringbuf. */
static void
write_prop_to_stringbuf (svn_stringbuf_t **strbuf,
                         const char *name,
                         const svn_string_t *value)
{
  int bytes_used, namelen;
  char buf[SVN_KEYLINE_MAXLEN];

  /* Output name length, then name. */
  namelen = strlen (name);
  svn_stringbuf_appendbytes (*strbuf, "K ", 2);

  sprintf (buf, "%d%n", namelen, &bytes_used);
  svn_stringbuf_appendbytes (*strbuf, buf, bytes_used);
  svn_stringbuf_appendbytes (*strbuf, "\n", 1);

  svn_stringbuf_appendbytes (*strbuf, name, namelen);
  svn_stringbuf_appendbytes (*strbuf, "\n", 1);

  /* Output value length, then value. */
  svn_stringbuf_appendbytes (*strbuf, "V ", 2);

  sprintf (buf, "%" APR_SIZE_T_FMT "%n", value->len, &bytes_used);
  svn_stringbuf_appendbytes (*strbuf, buf, bytes_used);
  svn_stringbuf_appendbytes (*strbuf, "\n", 1);

  svn_stringbuf_appendbytes (*strbuf, value->data, value->len);
  svn_stringbuf_appendbytes (*strbuf, "\n", 1);
}


/* Prefix matching function to compare node-path with set of prefixes. */
static svn_boolean_t
ary_prefix_match (apr_array_header_t *pfxlist, const char *path)
{
  int i, pfx_len, path_len = strlen (path);
  const char *pfx;

  for (i = 0; i < pfxlist->nelts; i++)
    {
      pfx = APR_ARRAY_IDX (pfxlist, i, const char *);
      pfx_len = strlen (pfx);
      if (path_len < pfx_len)
        continue;
      if (strncmp (path, pfx, pfx_len) == 0)
        return TRUE;
    }

  return FALSE;
}




/* Note: the input stream parser calls us up with events.  Output of
   filtered dump should take place at the close-events.  Before that
   we just save supplied data in corresponding batons.
*/


/* Filtering batons */

struct parse_baton_t 
{
  /* Command-line options values. */
  svn_boolean_t do_exclude;
  svn_boolean_t quiet;
  svn_boolean_t drop_empty_revs;
  svn_boolean_t do_renumber_revs;
  svn_boolean_t preserve_revprops;
  apr_array_header_t *prefixes;

  /* Input and output streams. */
  svn_stream_t *in_stream;
  svn_stream_t *out_stream;

  /* State for the filtering process. */
  apr_int32_t rev_drop_count;
  apr_hash_t *dropped_nodes;
  apr_hash_t *renumber_history;
};

struct revision_baton_t 
{
  /* Reference to the global parse baton. */
  struct parse_baton_t *pb;

  /* Does this revision have node or prop changes? */
  svn_boolean_t has_nodes;
  svn_boolean_t has_props;

  /* Did we drop any nodes? */
  svn_boolean_t had_dropped_nodes;

  /* The original and new (re-mapped) revision numbers. */
  svn_revnum_t rev_orig;
  svn_revnum_t rev_actual;

  /* Pointers to dumpfile data. */
  svn_stringbuf_t *header;
  apr_hash_t *props;
  svn_stringbuf_t *body;
  svn_stream_t *body_stream;
};

struct node_baton_t 
{
  /* Reference to the current revision baton. */
  struct revision_baton_t *rb;

  /* Are we skipping this node? */
  svn_boolean_t do_skip;

  /* Have we been instructed to change or remove props on, or change
     the text of, this node? */
  svn_boolean_t has_props;
  svn_boolean_t has_text;

  /* Pointers to dumpfile data. */
  svn_stringbuf_t *header;
  svn_stringbuf_t *props;
  svn_stringbuf_t *body;
  svn_stringbuf_t *node_path;
  svn_stringbuf_t *copyfrom_path;
  svn_stream_t *body_stream;
};




/* Filtering vtable members */

/* New revision: set up revision_baton, decide if we skip it. */
static svn_error_t *
new_revision_record (void **revision_baton,
                     apr_hash_t *headers,
                     void *parse_baton,
                     apr_pool_t *pool)
{
  struct revision_baton_t *rb;
  apr_hash_index_t *hi;
  const void *key;
  void *val;
  svn_stream_t *header_stream;

  apr_pool_t *revhistory_pool;
  svn_revnum_t *rr_key, *rr_value;

  *revision_baton = apr_palloc (pool, sizeof (struct revision_baton_t));
  rb = *revision_baton;
  rb->has_nodes = FALSE;
  rb->has_props = FALSE;
  rb->had_dropped_nodes = FALSE;
  rb->pb     = parse_baton;
  rb->header = svn_stringbuf_create ("", pool);
  rb->body   = svn_stringbuf_create ("", pool);
  rb->props  = apr_hash_make (pool);
  rb->body_stream = svn_stream_from_stringbuf (rb->body, pool);

  header_stream = svn_stream_from_stringbuf (rb->header, pool);

  val = apr_hash_get (headers, SVN_REPOS_DUMPFILE_REVISION_NUMBER,
                      APR_HASH_KEY_STRING);
  rb->rev_orig = SVN_STR_TO_REV(val);

  if (rb->pb->do_renumber_revs)
    {
      rb->rev_actual = rb->rev_orig - rb->pb->rev_drop_count;

      revhistory_pool = apr_hash_pool_get (rb->pb->renumber_history);
      rr_key = apr_palloc (revhistory_pool, sizeof (svn_revnum_t));
      rr_value = apr_palloc (revhistory_pool, sizeof (svn_revnum_t));

      *rr_key = rb->rev_orig;
      *rr_value = rb->rev_actual;

      apr_hash_set (rb->pb->renumber_history, rr_key,
                    sizeof (svn_revnum_t), rr_value);
    }
  else
    {
      rb->rev_actual = rb->rev_orig;
    }


  SVN_ERR (svn_stream_printf (header_stream, pool,
                              SVN_REPOS_DUMPFILE_REVISION_NUMBER ": %ld\n",
                              rb->rev_actual));

  for (hi = apr_hash_first (pool, headers); hi; hi = apr_hash_next (hi))
    {
      apr_hash_this (hi, &key, NULL, &val);
      if ((!strcmp (key, SVN_REPOS_DUMPFILE_CONTENT_LENGTH))
          || (!strcmp (key, SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH))
          || (!strcmp (key, SVN_REPOS_DUMPFILE_REVISION_NUMBER)))
        continue;

      /* passthru: put header into header stringbuf. */

      SVN_ERR (svn_stream_printf (header_stream, pool, "%s: %s\n",
                                  (const char *)key,
                                  (const char *)val));

    }

  SVN_ERR (svn_stream_close (header_stream));

  return SVN_NO_ERROR;
}


/* UUID record here: dump it, as we do not filter them. */
static svn_error_t *
uuid_record (const char *uuid, void *parse_baton, apr_pool_t *pool)
{
  struct parse_baton_t *pb = parse_baton;
  SVN_ERR (svn_stream_printf (pb->out_stream, pool,
                              SVN_REPOS_DUMPFILE_UUID ": %s\n\n", uuid));
  return SVN_NO_ERROR;
}


/* New node here. Set up node_baton by copying headers. */
static svn_error_t *
new_node_record (void **node_baton,
                 apr_hash_t *headers,
                 void *rev_baton,
                 apr_pool_t *pool)
{
  struct parse_baton_t *pb;
  struct node_baton_t *nb;
  char *node_path, *copyfrom_path;
  svn_revnum_t cf_orig_rev, *cf_renum_rev;
  apr_hash_index_t *hi;
  const void *key;
  void *val;
  svn_stream_t *header_stream;

  *node_baton = apr_palloc (pool, sizeof (struct node_baton_t));
  nb          = *node_baton;
  nb->rb      = rev_baton;
  pb          = nb->rb->pb;

  node_path = apr_hash_get (headers, SVN_REPOS_DUMPFILE_NODE_PATH,
                            APR_HASH_KEY_STRING);
  copyfrom_path = apr_hash_get (headers,
                                SVN_REPOS_DUMPFILE_NODE_COPYFROM_PATH,
                                APR_HASH_KEY_STRING);

  /* Ensure that paths start with a leading '/'. */
  node_path = svn_path_join ("/", node_path, pool);
  if (copyfrom_path)
    copyfrom_path = svn_path_join ("/", copyfrom_path, pool);

  /* Shame, shame, shame ... this is NXOR. */
  nb->do_skip = (ary_prefix_match (pb->prefixes, node_path)
                 ? pb->do_exclude : (! pb->do_exclude));

  /* If we're skipping the node, take note of path, discarding the
     rest.  */
  if (nb->do_skip)
    {
      apr_hash_set (pb->dropped_nodes, 
                    apr_pstrdup (apr_hash_pool_get (pb->dropped_nodes), 
                                 node_path),
                    APR_HASH_KEY_STRING, (void *)1);
      nb->rb->had_dropped_nodes = TRUE;
    }
  else
    {
      /* Test if this node was copied from dropped source. */
      if (copyfrom_path &&
          (ary_prefix_match (pb->prefixes, copyfrom_path) 
           ? pb->do_exclude : (! pb->do_exclude)))
        {
          /* This node was copied from dropped source.
             We have a problem, since we did not want to drop this node too.

             However, there is one special case we'll handle.  If the node is
             a file, and this was a copy-and-modify operation, then the
             dumpfile should contain the new contents of the file.  In this
             scenario, we'll just do an add without history using the new
             contents.  */
          const char *kind, *tcl;
          kind = apr_hash_get (headers, SVN_REPOS_DUMPFILE_NODE_KIND,
                               APR_HASH_KEY_STRING);
          tcl = apr_hash_get (headers, SVN_REPOS_DUMPFILE_TEXT_CONTENT_LENGTH,
                              APR_HASH_KEY_STRING);

          /* If there is a Text-content-length header, and the kind is
             "file", we just fallback to an add without history. */
          if (tcl && (strcmp (kind, "file") == 0))
            {
              apr_hash_set (headers, SVN_REPOS_DUMPFILE_NODE_COPYFROM_PATH,
                            APR_HASH_KEY_STRING, NULL);
              apr_hash_set (headers, SVN_REPOS_DUMPFILE_NODE_COPYFROM_REV,
                            APR_HASH_KEY_STRING, NULL);
              copyfrom_path = NULL;
            }
          /* Else, this is either a directory or a file whose contents we
             don't have readily available.  */
          else
            {
              return svn_error_createf 
                (SVN_ERR_INCOMPLETE_DATA, 0,
                 _("Invalid copy source path '%s'"), copyfrom_path);
            }
        }

      nb->has_props = FALSE;
      nb->has_text  = FALSE;
      nb->header    = svn_stringbuf_create ("", pool);
      nb->props     = svn_stringbuf_create ("", pool);
      nb->body      = svn_stringbuf_create ("", pool);
      nb->body_stream = svn_stream_from_stringbuf (nb->body, pool);
      header_stream  = svn_stream_from_stringbuf (nb->header, pool);

      for (hi = apr_hash_first (pool, headers); hi; hi = apr_hash_next (hi))
        {
          apr_hash_this (hi, (const void **) &key, NULL, &val);
          if ((!strcmp (key, SVN_REPOS_DUMPFILE_CONTENT_LENGTH))
              || (!strcmp (key, SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH))
              || (!strcmp (key, SVN_REPOS_DUMPFILE_TEXT_CONTENT_LENGTH)))
            continue;

          /* Rewrite Node-Copyfrom-Rev if we are renumbering revisions.
             The number points to some revision in the past. We keep track
             of revision renumbering in an apr_hash, which maps original
             revisions to new ones. Dropped revision are mapped to -1.
             This should never happen here.
          */
          if (pb->do_renumber_revs
              && (!strcmp (key, SVN_REPOS_DUMPFILE_NODE_COPYFROM_REV)))
            {
              cf_orig_rev = SVN_STR_TO_REV(val);
              cf_renum_rev = apr_hash_get (pb->renumber_history,
                                           &cf_orig_rev,
                                           sizeof (svn_revnum_t));
              if ((cf_renum_rev == NULL) || (*cf_renum_rev == -1))
                {
                  /* bail out with an error */
                  return svn_error_createf
                    (SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
                     _("Node with dropped parent sneaked in"));
                }
              SVN_ERR (svn_stream_printf
                       (header_stream, pool,
                        SVN_REPOS_DUMPFILE_NODE_COPYFROM_REV ": %ld\n",
                        *cf_renum_rev));
              continue;
            }

          /* passthru: put header into header stringbuf  */

          SVN_ERR (svn_stream_printf (header_stream, pool, "%s: %s\n",
                                      (const char *)key,
                                      (const char *)val));
        }

      SVN_ERR (svn_stream_close (header_stream));
    }

  return SVN_NO_ERROR;
}


static svn_error_t *
set_revision_property (void *revision_baton,
                       const char *name,
                       const svn_string_t *value)
{
  struct revision_baton_t *rb = revision_baton;
  apr_pool_t *hash_pool = apr_hash_pool_get (rb->props);

  rb->has_props = TRUE;
  apr_hash_set (rb->props, apr_pstrdup (hash_pool, name),
                APR_HASH_KEY_STRING, svn_string_dup (value, hash_pool));
  return SVN_NO_ERROR;
}


static svn_error_t *
set_node_property (void *node_baton,
                   const char *name,
                   const svn_string_t *value)
{
  struct node_baton_t *nb = node_baton;

  if (nb->do_skip)
    return SVN_NO_ERROR;

  if (!nb->has_props)
    return svn_error_create (SVN_ERR_UNSUPPORTED_FEATURE, NULL,
                             _("Delta property block detected - "
                               "not supported by svndumpfilter"));

  write_prop_to_stringbuf (&(nb->props), name, value);

  return SVN_NO_ERROR;
}


static svn_error_t *
remove_node_props (void *node_baton)
{
  struct node_baton_t *nb = node_baton;

  /* In this case, not actually indicating that the node *has* props,
     rather that we know about all the props that it has, since it now
     has none. */
  nb->has_props = TRUE;

  return SVN_NO_ERROR;
}


static svn_error_t *
set_fulltext (svn_stream_t **stream, void *node_baton)
{
  struct node_baton_t *nb = node_baton;

  if (!nb->do_skip)
    {
      *stream = nb->body_stream;
      nb->has_text = TRUE;
    }

  return SVN_NO_ERROR;
}


/* Finalize node */
static svn_error_t *
close_node (void *node_baton)
{
  struct node_baton_t *nb = node_baton;
  int bytes_used;
  char buf[SVN_KEYLINE_MAXLEN];

  /* Get out of here if we can. */
  if (nb->do_skip)
    {
      return SVN_NO_ERROR;
    }

  /* when there are no props nb->props->len would be zero and won't mess up
     Content-Length. */
  if (nb->has_props)
    svn_stringbuf_appendcstr (nb->props, "PROPS-END\n");

  /* 1. recalculate & check text-md5 if present. Passed through right now. */

  /* 2. recalculate and add content-lengths */

  if (nb->has_props)
    {
      svn_stringbuf_appendcstr (nb->header,
                                SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH);
      sprintf (buf, ": %" APR_SIZE_T_FMT "%n", nb->props->len, &bytes_used);
      svn_stringbuf_appendbytes (nb->header, buf, bytes_used);
      svn_stringbuf_appendbytes (nb->header, "\n", 1);
    }
  if (nb->has_text)
    {
      svn_stringbuf_appendcstr (nb->header,
                                SVN_REPOS_DUMPFILE_TEXT_CONTENT_LENGTH);
      sprintf (buf, ": %" APR_SIZE_T_FMT "%n", nb->body->len, &bytes_used);
      svn_stringbuf_appendbytes (nb->header, buf, bytes_used);
      svn_stringbuf_appendbytes (nb->header, "\n", 1);
    }
  svn_stringbuf_appendcstr (nb->header, SVN_REPOS_DUMPFILE_CONTENT_LENGTH);
  sprintf (buf, ": %" APR_SIZE_T_FMT "%n", (nb->props->len + nb->body->len),
           &bytes_used);
  svn_stringbuf_appendbytes (nb->header, buf, bytes_used);
  svn_stringbuf_appendbytes (nb->header, "\n", 1);

  /* put an end to headers */
  svn_stringbuf_appendbytes (nb->header, "\n", 1);

  /* put an end to node. */
  svn_stringbuf_appendbytes (nb->body,   "\n\n", 2);

  /* 3. add all stuff to the parent revision */

  svn_stringbuf_appendstr (nb->rb->body, nb->header);
  svn_stringbuf_appendstr (nb->rb->body, nb->props);
  svn_stringbuf_appendstr (nb->rb->body, nb->body);
  nb->rb->has_nodes = TRUE;

  return SVN_NO_ERROR;
}

/* Finalize revision */
static svn_error_t *
close_revision (void *revision_baton)
{
  struct revision_baton_t *rb = revision_baton;
  int bytes_used;
  char buf[SVN_KEYLINE_MAXLEN];
  apr_hash_index_t *hi;
  apr_pool_t *hash_pool = apr_hash_pool_get (rb->props);
  svn_stringbuf_t *props = svn_stringbuf_create ("", hash_pool);
  apr_pool_t *subpool = svn_pool_create (hash_pool);

  /* If this revision has no nodes left because the ones it had were
     dropped, and we are not dropping empty revisions, and we were not
     told to preserve revision props, then we want to fixup the
     revision props to only contain:
       - the date
       - a log message that reports that this revision is just stuffing. */
  if ((! rb->pb->preserve_revprops)
      && (! rb->has_nodes) 
      && rb->had_dropped_nodes 
      && (! rb->pb->drop_empty_revs))
    {
      apr_hash_t *old_props = rb->props;
      rb->has_props = TRUE;
      rb->props = apr_hash_make (hash_pool);
      apr_hash_set (rb->props, SVN_PROP_REVISION_DATE, APR_HASH_KEY_STRING,
                    apr_hash_get (old_props, SVN_PROP_REVISION_DATE, 
                                  APR_HASH_KEY_STRING));
      apr_hash_set (rb->props, SVN_PROP_REVISION_LOG, APR_HASH_KEY_STRING,
                    svn_string_create (_("This is an empty revision for "
                                         "padding."), hash_pool));
    }

  /* Now, "rasterize" the props to a string, and append the property
     information to the header string.  */
  if (rb->has_props)
    {
      for (hi = apr_hash_first (subpool, rb->props); 
           hi; 
           hi = apr_hash_next (hi))
        {
          const void *key;
          void *val;
          apr_hash_this (hi, &key, NULL, &val);
          write_prop_to_stringbuf (&props, key, val);
        }
      svn_stringbuf_appendcstr (props, "PROPS-END\n");
      svn_stringbuf_appendcstr (rb->header,
                                SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH);
      sprintf (buf, ": %" APR_SIZE_T_FMT "%n", props->len, &bytes_used);
      svn_stringbuf_appendbytes (rb->header, buf, bytes_used);
      svn_stringbuf_appendbytes (rb->header, "\n", 1);
    }

  svn_stringbuf_appendcstr (rb->header, SVN_REPOS_DUMPFILE_CONTENT_LENGTH);
  sprintf (buf, ": %" APR_SIZE_T_FMT "%n", props->len, &bytes_used);
  svn_stringbuf_appendbytes (rb->header, buf, bytes_used);
  svn_stringbuf_appendbytes (rb->header, "\n", 1);

  /* put an end to headers */
  svn_stringbuf_appendbytes (rb->header, "\n", 1);

  /* put an end to revision */
  svn_stringbuf_appendbytes (props,  "\n", 1);

  /* write out the revision */
  /* Revision is written out in the following cases:
     1. No --drop-empty-revs has been supplied.
     2. --drop-empty-revs has been supplied,
     but revision has not all nodes dropped
     3. Revision had no nodes to begin with.
  */
  if (rb->has_nodes
      || (! rb->pb->drop_empty_revs)
      || (! rb->had_dropped_nodes))
    {
      SVN_ERR (svn_stream_write (rb->pb->out_stream,
                                 rb->header->data , &(rb->header->len)));
      SVN_ERR (svn_stream_write (rb->pb->out_stream,
                                 props->data      , &(props->len)));
      SVN_ERR (svn_stream_write (rb->pb->out_stream,
                                 rb->body->data   , &(rb->body->len)));
      if (! rb->pb->quiet)
        SVN_ERR (svn_cmdline_fprintf (stderr, subpool,
                                      _("Revision %ld committed as %ld.\n"),
                                      rb->rev_orig, rb->rev_actual));
    }
  else
    {
      rb->pb->rev_drop_count++;
      if (! rb->pb->quiet)
        SVN_ERR (svn_cmdline_fprintf (stderr, subpool,
                                      _("Revision %ld skipped.\n"),
                                      rb->rev_orig));
    }
  svn_pool_destroy (subpool);
  return SVN_NO_ERROR;<commit_msg>Despite the apparent complexity of the diff, this commit is just moving whole functions up and down the file without change, in order to increase the reviewability of a future commit's diff.<commit_after>/*
 * main.c: Subversion dump stream filtering tool.
 *
 * ====================================================================
 * Copyright (c) 2000-2004 CollabNet.  All rights reserved.
 *
 * This software is licensed as described in the file COPYING, which
 * you should have received as part of this distribution.  The terms
 * are also available at http://subversion.tigris.org/license-1.html.
 * If newer versions of this license are posted there, you may use a
 * newer version instead, at your option.
 *
 * This software consists of voluntary contributions made by many
 * individuals.  For exact contribution history, see the revision
 * history and logs, available at http://subversion.tigris.org/.
 * ====================================================================
 */


#include <stdlib.h>

#include <apr_file_io.h>

#include "svn_private_config.h"
#include "svn_cmdline.h"
#include "svn_error.h"
#include "svn_opt.h"
#include "svn_utf.h"
#include "svn_path.h"
#include "svn_hash.h"
#include "svn_repos.h"
#include "svn_pools.h"
#include "svn_sorts.h"
#include "svn_props.h"



/*** Code. ***/

/* Helper to open stdio streams */

/* NOTE: we used to call svn_stream_from_stdio(), which wraps a stream
   around a standard stdio.h FILE pointer.  The problem is that these
   pointers operate through C Run Time (CRT) on Win32, which does all
   sorts of translation on them: LF's become CRLF's, and ctrl-Z's
   embedded in Word documents are interpreted as premature EOF's.

   So instead, we use apr_file_open_std*, which bypass the CRT and
   directly wrap the OS's file-handles, which don't know or care about
   translation.  Thus dump/load works correctly on Win32.
*/
static svn_error_t *
create_stdio_stream (svn_stream_t **stream,
                     APR_DECLARE(apr_status_t) open_fn (apr_file_t **,
                                                        apr_pool_t *),
                     apr_pool_t *pool)
{
  apr_file_t *stdio_file;
  apr_status_t apr_err = open_fn (&stdio_file, pool);

  if (apr_err)
    return svn_error_wrap_apr (apr_err, _("Can't open stdio file"));

  *stream = svn_stream_from_aprfile (stdio_file, pool);
  return SVN_NO_ERROR;
}


/* Writes a property in dumpfile format to given stringbuf. */
static void
write_prop_to_stringbuf (svn_stringbuf_t **strbuf,
                         const char *name,
                         const svn_string_t *value)
{
  int bytes_used, namelen;
  char buf[SVN_KEYLINE_MAXLEN];

  /* Output name length, then name. */
  namelen = strlen (name);
  svn_stringbuf_appendbytes (*strbuf, "K ", 2);

  sprintf (buf, "%d%n", namelen, &bytes_used);
  svn_stringbuf_appendbytes (*strbuf, buf, bytes_used);
  svn_stringbuf_appendbytes (*strbuf, "\n", 1);

  svn_stringbuf_appendbytes (*strbuf, name, namelen);
  svn_stringbuf_appendbytes (*strbuf, "\n", 1);

  /* Output value length, then value. */
  svn_stringbuf_appendbytes (*strbuf, "V ", 2);

  sprintf (buf, "%" APR_SIZE_T_FMT "%n", value->len, &bytes_used);
  svn_stringbuf_appendbytes (*strbuf, buf, bytes_used);
  svn_stringbuf_appendbytes (*strbuf, "\n", 1);

  svn_stringbuf_appendbytes (*strbuf, value->data, value->len);
  svn_stringbuf_appendbytes (*strbuf, "\n", 1);
}


/* Prefix matching function to compare node-path with set of prefixes. */
static svn_boolean_t
ary_prefix_match (apr_array_header_t *pfxlist, const char *path)
{
  int i, pfx_len, path_len = strlen (path);
  const char *pfx;

  for (i = 0; i < pfxlist->nelts; i++)
    {
      pfx = APR_ARRAY_IDX (pfxlist, i, const char *);
      pfx_len = strlen (pfx);
      if (path_len < pfx_len)
        continue;
      if (strncmp (path, pfx, pfx_len) == 0)
        return TRUE;
    }

  return FALSE;
}




/* Note: the input stream parser calls us up with events.  Output of
   filtered dump should take place at the close-events.  Before that
   we just save supplied data in corresponding batons.
*/


/* Filtering batons */

struct parse_baton_t 
{
  /* Command-line options values. */
  svn_boolean_t do_exclude;
  svn_boolean_t quiet;
  svn_boolean_t drop_empty_revs;
  svn_boolean_t do_renumber_revs;
  svn_boolean_t preserve_revprops;
  apr_array_header_t *prefixes;

  /* Input and output streams. */
  svn_stream_t *in_stream;
  svn_stream_t *out_stream;

  /* State for the filtering process. */
  apr_int32_t rev_drop_count;
  apr_hash_t *dropped_nodes;
  apr_hash_t *renumber_history;
};

struct revision_baton_t 
{
  /* Reference to the global parse baton. */
  struct parse_baton_t *pb;

  /* Does this revision have node or prop changes? */
  svn_boolean_t has_nodes;
  svn_boolean_t has_props;

  /* Did we drop any nodes? */
  svn_boolean_t had_dropped_nodes;

  /* The original and new (re-mapped) revision numbers. */
  svn_revnum_t rev_orig;
  svn_revnum_t rev_actual;

  /* Pointers to dumpfile data. */
  svn_stringbuf_t *header;
  apr_hash_t *props;
  svn_stringbuf_t *body;
  svn_stream_t *body_stream;
};

struct node_baton_t 
{
  /* Reference to the current revision baton. */
  struct revision_baton_t *rb;

  /* Are we skipping this node? */
  svn_boolean_t do_skip;

  /* Have we been instructed to change or remove props on, or change
     the text of, this node? */
  svn_boolean_t has_props;
  svn_boolean_t has_text;

  /* Pointers to dumpfile data. */
  svn_stringbuf_t *header;
  svn_stringbuf_t *props;
  svn_stringbuf_t *body;
  svn_stringbuf_t *node_path;
  svn_stringbuf_t *copyfrom_path;
  svn_stream_t *body_stream;
};




/* Filtering vtable members */

/* New revision: set up revision_baton, decide if we skip it. */
static svn_error_t *
new_revision_record (void **revision_baton,
                     apr_hash_t *headers,
                     void *parse_baton,
                     apr_pool_t *pool)
{
  struct revision_baton_t *rb;
  apr_hash_index_t *hi;
  const void *key;
  void *val;
  svn_stream_t *header_stream;

  apr_pool_t *revhistory_pool;
  svn_revnum_t *rr_key, *rr_value;

  *revision_baton = apr_palloc (pool, sizeof (struct revision_baton_t));
  rb = *revision_baton;
  rb->has_nodes = FALSE;
  rb->has_props = FALSE;
  rb->had_dropped_nodes = FALSE;
  rb->pb     = parse_baton;
  rb->header = svn_stringbuf_create ("", pool);
  rb->body   = svn_stringbuf_create ("", pool);
  rb->props  = apr_hash_make (pool);
  rb->body_stream = svn_stream_from_stringbuf (rb->body, pool);

  header_stream = svn_stream_from_stringbuf (rb->header, pool);

  val = apr_hash_get (headers, SVN_REPOS_DUMPFILE_REVISION_NUMBER,
                      APR_HASH_KEY_STRING);
  rb->rev_orig = SVN_STR_TO_REV(val);

  if (rb->pb->do_renumber_revs)
    {
      rb->rev_actual = rb->rev_orig - rb->pb->rev_drop_count;

      revhistory_pool = apr_hash_pool_get (rb->pb->renumber_history);
      rr_key = apr_palloc (revhistory_pool, sizeof (svn_revnum_t));
      rr_value = apr_palloc (revhistory_pool, sizeof (svn_revnum_t));

      *rr_key = rb->rev_orig;
      *rr_value = rb->rev_actual;

      apr_hash_set (rb->pb->renumber_history, rr_key,
                    sizeof (svn_revnum_t), rr_value);
    }
  else
    {
      rb->rev_actual = rb->rev_orig;
    }


  SVN_ERR (svn_stream_printf (header_stream, pool,
                              SVN_REPOS_DUMPFILE_REVISION_NUMBER ": %ld\n",
                              rb->rev_actual));

  for (hi = apr_hash_first (pool, headers); hi; hi = apr_hash_next (hi))
    {
      apr_hash_this (hi, &key, NULL, &val);
      if ((!strcmp (key, SVN_REPOS_DUMPFILE_CONTENT_LENGTH))
          || (!strcmp (key, SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH))
          || (!strcmp (key, SVN_REPOS_DUMPFILE_REVISION_NUMBER)))
        continue;

      /* passthru: put header into header stringbuf. */

      SVN_ERR (svn_stream_printf (header_stream, pool, "%s: %s\n",
                                  (const char *)key,
                                  (const char *)val));

    }

  SVN_ERR (svn_stream_close (header_stream));

  return SVN_NO_ERROR;
}


/* Finalize revision */
static svn_error_t *
close_revision (void *revision_baton)
{
  struct revision_baton_t *rb = revision_baton;
  int bytes_used;
  char buf[SVN_KEYLINE_MAXLEN];
  apr_hash_index_t *hi;
  apr_pool_t *hash_pool = apr_hash_pool_get (rb->props);
  svn_stringbuf_t *props = svn_stringbuf_create ("", hash_pool);
  apr_pool_t *subpool = svn_pool_create (hash_pool);

  /* If this revision has no nodes left because the ones it had were
     dropped, and we are not dropping empty revisions, and we were not
     told to preserve revision props, then we want to fixup the
     revision props to only contain:
       - the date
       - a log message that reports that this revision is just stuffing. */
  if ((! rb->pb->preserve_revprops)
      && (! rb->has_nodes) 
      && rb->had_dropped_nodes 
      && (! rb->pb->drop_empty_revs))
    {
      apr_hash_t *old_props = rb->props;
      rb->has_props = TRUE;
      rb->props = apr_hash_make (hash_pool);
      apr_hash_set (rb->props, SVN_PROP_REVISION_DATE, APR_HASH_KEY_STRING,
                    apr_hash_get (old_props, SVN_PROP_REVISION_DATE, 
                                  APR_HASH_KEY_STRING));
      apr_hash_set (rb->props, SVN_PROP_REVISION_LOG, APR_HASH_KEY_STRING,
                    svn_string_create (_("This is an empty revision for "
                                         "padding."), hash_pool));
    }

  /* Now, "rasterize" the props to a string, and append the property
     information to the header string.  */
  if (rb->has_props)
    {
      for (hi = apr_hash_first (subpool, rb->props); 
           hi; 
           hi = apr_hash_next (hi))
        {
          const void *key;
          void *val;
          apr_hash_this (hi, &key, NULL, &val);
          write_prop_to_stringbuf (&props, key, val);
        }
      svn_stringbuf_appendcstr (props, "PROPS-END\n");
      svn_stringbuf_appendcstr (rb->header,
                                SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH);
      sprintf (buf, ": %" APR_SIZE_T_FMT "%n", props->len, &bytes_used);
      svn_stringbuf_appendbytes (rb->header, buf, bytes_used);
      svn_stringbuf_appendbytes (rb->header, "\n", 1);
    }

  svn_stringbuf_appendcstr (rb->header, SVN_REPOS_DUMPFILE_CONTENT_LENGTH);
  sprintf (buf, ": %" APR_SIZE_T_FMT "%n", props->len, &bytes_used);
  svn_stringbuf_appendbytes (rb->header, buf, bytes_used);
  svn_stringbuf_appendbytes (rb->header, "\n", 1);

  /* put an end to headers */
  svn_stringbuf_appendbytes (rb->header, "\n", 1);

  /* put an end to revision */
  svn_stringbuf_appendbytes (props,  "\n", 1);

  /* write out the revision */
  /* Revision is written out in the following cases:
     1. No --drop-empty-revs has been supplied.
     2. --drop-empty-revs has been supplied,
     but revision has not all nodes dropped
     3. Revision had no nodes to begin with.
  */
  if (rb->has_nodes
      || (! rb->pb->drop_empty_revs)
      || (! rb->had_dropped_nodes))
    {
      SVN_ERR (svn_stream_write (rb->pb->out_stream,
                                 rb->header->data , &(rb->header->len)));
      SVN_ERR (svn_stream_write (rb->pb->out_stream,
                                 props->data      , &(props->len)));
      SVN_ERR (svn_stream_write (rb->pb->out_stream,
                                 rb->body->data   , &(rb->body->len)));
      if (! rb->pb->quiet)
        SVN_ERR (svn_cmdline_fprintf (stderr, subpool,
                                      _("Revision %ld committed as %ld.\n"),
                                      rb->rev_orig, rb->rev_actual));
    }
  else
    {
      rb->pb->rev_drop_count++;
      if (! rb->pb->quiet)
        SVN_ERR (svn_cmdline_fprintf (stderr, subpool,
                                      _("Revision %ld skipped.\n"),
                                      rb->rev_orig));
    }
  svn_pool_destroy (subpool);
  return SVN_NO_ERROR;
}


/* UUID record here: dump it, as we do not filter them. */
static svn_error_t *
uuid_record (const char *uuid, void *parse_baton, apr_pool_t *pool)
{
  struct parse_baton_t *pb = parse_baton;
  SVN_ERR (svn_stream_printf (pb->out_stream, pool,
                              SVN_REPOS_DUMPFILE_UUID ": %s\n\n", uuid));
  return SVN_NO_ERROR;
}


/* New node here. Set up node_baton by copying headers. */
static svn_error_t *
new_node_record (void **node_baton,
                 apr_hash_t *headers,
                 void *rev_baton,
                 apr_pool_t *pool)
{
  struct parse_baton_t *pb;
  struct node_baton_t *nb;
  char *node_path, *copyfrom_path;
  svn_revnum_t cf_orig_rev, *cf_renum_rev;
  apr_hash_index_t *hi;
  const void *key;
  void *val;
  svn_stream_t *header_stream;

  *node_baton = apr_palloc (pool, sizeof (struct node_baton_t));
  nb          = *node_baton;
  nb->rb      = rev_baton;
  pb          = nb->rb->pb;

  node_path = apr_hash_get (headers, SVN_REPOS_DUMPFILE_NODE_PATH,
                            APR_HASH_KEY_STRING);
  copyfrom_path = apr_hash_get (headers,
                                SVN_REPOS_DUMPFILE_NODE_COPYFROM_PATH,
                                APR_HASH_KEY_STRING);

  /* Ensure that paths start with a leading '/'. */
  node_path = svn_path_join ("/", node_path, pool);
  if (copyfrom_path)
    copyfrom_path = svn_path_join ("/", copyfrom_path, pool);

  /* Shame, shame, shame ... this is NXOR. */
  nb->do_skip = (ary_prefix_match (pb->prefixes, node_path)
                 ? pb->do_exclude : (! pb->do_exclude));

  /* If we're skipping the node, take note of path, discarding the
     rest.  */
  if (nb->do_skip)
    {
      apr_hash_set (pb->dropped_nodes, 
                    apr_pstrdup (apr_hash_pool_get (pb->dropped_nodes), 
                                 node_path),
                    APR_HASH_KEY_STRING, (void *)1);
      nb->rb->had_dropped_nodes = TRUE;
    }
  else
    {
      /* Test if this node was copied from dropped source. */
      if (copyfrom_path &&
          (ary_prefix_match (pb->prefixes, copyfrom_path) 
           ? pb->do_exclude : (! pb->do_exclude)))
        {
          /* This node was copied from dropped source.
             We have a problem, since we did not want to drop this node too.

             However, there is one special case we'll handle.  If the node is
             a file, and this was a copy-and-modify operation, then the
             dumpfile should contain the new contents of the file.  In this
             scenario, we'll just do an add without history using the new
             contents.  */
          const char *kind, *tcl;
          kind = apr_hash_get (headers, SVN_REPOS_DUMPFILE_NODE_KIND,
                               APR_HASH_KEY_STRING);
          tcl = apr_hash_get (headers, SVN_REPOS_DUMPFILE_TEXT_CONTENT_LENGTH,
                              APR_HASH_KEY_STRING);

          /* If there is a Text-content-length header, and the kind is
             "file", we just fallback to an add without history. */
          if (tcl && (strcmp (kind, "file") == 0))
            {
              apr_hash_set (headers, SVN_REPOS_DUMPFILE_NODE_COPYFROM_PATH,
                            APR_HASH_KEY_STRING, NULL);
              apr_hash_set (headers, SVN_REPOS_DUMPFILE_NODE_COPYFROM_REV,
                            APR_HASH_KEY_STRING, NULL);
              copyfrom_path = NULL;
            }
          /* Else, this is either a directory or a file whose contents we
             don't have readily available.  */
          else
            {
              return svn_error_createf 
                (SVN_ERR_INCOMPLETE_DATA, 0,
                 _("Invalid copy source path '%s'"), copyfrom_path);
            }
        }

      nb->has_props = FALSE;
      nb->has_text  = FALSE;
      nb->header    = svn_stringbuf_create ("", pool);
      nb->props     = svn_stringbuf_create ("", pool);
      nb->body      = svn_stringbuf_create ("", pool);
      nb->body_stream = svn_stream_from_stringbuf (nb->body, pool);
      header_stream  = svn_stream_from_stringbuf (nb->header, pool);

      for (hi = apr_hash_first (pool, headers); hi; hi = apr_hash_next (hi))
        {
          apr_hash_this (hi, (const void **) &key, NULL, &val);
          if ((!strcmp (key, SVN_REPOS_DUMPFILE_CONTENT_LENGTH))
              || (!strcmp (key, SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH))
              || (!strcmp (key, SVN_REPOS_DUMPFILE_TEXT_CONTENT_LENGTH)))
            continue;

          /* Rewrite Node-Copyfrom-Rev if we are renumbering revisions.
             The number points to some revision in the past. We keep track
             of revision renumbering in an apr_hash, which maps original
             revisions to new ones. Dropped revision are mapped to -1.
             This should never happen here.
          */
          if (pb->do_renumber_revs
              && (!strcmp (key, SVN_REPOS_DUMPFILE_NODE_COPYFROM_REV)))
            {
              cf_orig_rev = SVN_STR_TO_REV(val);
              cf_renum_rev = apr_hash_get (pb->renumber_history,
                                           &cf_orig_rev,
                                           sizeof (svn_revnum_t));
              if ((cf_renum_rev == NULL) || (*cf_renum_rev == -1))
                {
                  /* bail out with an error */
                  return svn_error_createf
                    (SVN_ERR_NODE_UNEXPECTED_KIND, NULL,
                     _("Node with dropped parent sneaked in"));
                }
              SVN_ERR (svn_stream_printf
                       (header_stream, pool,
                        SVN_REPOS_DUMPFILE_NODE_COPYFROM_REV ": %ld\n",
                        *cf_renum_rev));
              continue;
            }

          /* passthru: put header into header stringbuf  */

          SVN_ERR (svn_stream_printf (header_stream, pool, "%s: %s\n",
                                      (const char *)key,
                                      (const char *)val));
        }

      SVN_ERR (svn_stream_close (header_stream));
    }

  return SVN_NO_ERROR;
}


/* Finalize node */
static svn_error_t *
close_node (void *node_baton)
{
  struct node_baton_t *nb = node_baton;
  int bytes_used;
  char buf[SVN_KEYLINE_MAXLEN];

  /* Get out of here if we can. */
  if (nb->do_skip)
    {
      return SVN_NO_ERROR;
    }

  /* when there are no props nb->props->len would be zero and won't mess up
     Content-Length. */
  if (nb->has_props)
    svn_stringbuf_appendcstr (nb->props, "PROPS-END\n");

  /* 1. recalculate & check text-md5 if present. Passed through right now. */

  /* 2. recalculate and add content-lengths */

  if (nb->has_props)
    {
      svn_stringbuf_appendcstr (nb->header,
                                SVN_REPOS_DUMPFILE_PROP_CONTENT_LENGTH);
      sprintf (buf, ": %" APR_SIZE_T_FMT "%n", nb->props->len, &bytes_used);
      svn_stringbuf_appendbytes (nb->header, buf, bytes_used);
      svn_stringbuf_appendbytes (nb->header, "\n", 1);
    }
  if (nb->has_text)
    {
      svn_stringbuf_appendcstr (nb->header,
                                SVN_REPOS_DUMPFILE_TEXT_CONTENT_LENGTH);
      sprintf (buf, ": %" APR_SIZE_T_FMT "%n", nb->body->len, &bytes_used);
      svn_stringbuf_appendbytes (nb->header, buf, bytes_used);
      svn_stringbuf_appendbytes (nb->header, "\n", 1);
    }
  svn_stringbuf_appendcstr (nb->header, SVN_REPOS_DUMPFILE_CONTENT_LENGTH);
  sprintf (buf, ": %" APR_SIZE_T_FMT "%n", (nb->props->len + nb->body->len),
           &bytes_used);
  svn_stringbuf_appendbytes (nb->header, buf, bytes_used);
  svn_stringbuf_appendbytes (nb->header, "\n", 1);

  /* put an end to headers */
  svn_stringbuf_appendbytes (nb->header, "\n", 1);

  /* put an end to node. */
  svn_stringbuf_appendbytes (nb->body,   "\n\n", 2);

  /* 3. add all stuff to the parent revision */

  svn_stringbuf_appendstr (nb->rb->body, nb->header);
  svn_stringbuf_appendstr (nb->rb->body, nb->props);
  svn_stringbuf_appendstr (nb->rb->body, nb->body);
  nb->rb->has_nodes = TRUE;

  return SVN_NO_ERROR;
}


static svn_error_t *
set_revision_property (void *revision_baton,
                       const char *name,
                       const svn_string_t *value)
{
  struct revision_baton_t *rb = revision_baton;
  apr_pool_t *hash_pool = apr_hash_pool_get (rb->props);

  rb->has_props = TRUE;
  apr_hash_set (rb->props, apr_pstrdup (hash_pool, name),
                APR_HASH_KEY_STRING, svn_string_dup (value, hash_pool));
  return SVN_NO_ERROR;
}


static svn_error_t *
set_node_property (void *node_baton,
                   const char *name,
                   const svn_string_t *value)
{
  struct node_baton_t *nb = node_baton;

  if (nb->do_skip)
    return SVN_NO_ERROR;

  if (!nb->has_props)
    return svn_error_create (SVN_ERR_UNSUPPORTED_FEATURE, NULL,
                             _("Delta property block detected - "
                               "not supported by svndumpfilter"));

  write_prop_to_stringbuf (&(nb->props), name, value);

  return SVN_NO_ERROR;
}


static svn_error_t *
remove_node_props (void *node_baton)
{
  struct node_baton_t *nb = node_baton;

  /* In this case, not actually indicating that the node *has* props,
     rather that we know about all the props that it has, since it now
     has none. */
  nb->has_props = TRUE;

  return SVN_NO_ERROR;
}


static svn_error_t *
set_fulltext (svn_stream_t **stream, void *node_baton)
{
  struct node_baton_t *nb = node_baton;

  if (!nb->do_skip)
    {
      *stream = nb->body_stream;
      nb->has_text = TRUE;
    }

  return SVN_NO_ERROR;
{'fraction_non_alphanumeric': 0.11034802784222737, 'fraction_numerical': 0.0020417633410672853, 'mean_word_length': 2.43653324828576, 'pattern_counts': {'":': 16, '<': 11, '<?xml version=': 0, '>': 317, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 42, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6063519', 'n_tokens_mistral': 14853, 'n_tokens_neox': 14516, 'n_words': 3564}