1 /* low_level.c --- low level r/w access to FSX file structures
2  *
3  * ====================================================================
4  *    Licensed to the Apache Software Foundation (ASF) under one
5  *    or more contributor license agreements.  See the NOTICE file
6  *    distributed with this work for additional information
7  *    regarding copyright ownership.  The ASF licenses this file
8  *    to you under the Apache License, Version 2.0 (the
9  *    "License"); you may not use this file except in compliance
10  *    with the License.  You may obtain a copy of the License at
11  *
12  *      http://www.apache.org/licenses/LICENSE-2.0
13  *
14  *    Unless required by applicable law or agreed to in writing,
15  *    software distributed under the License is distributed on an
16  *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17  *    KIND, either express or implied.  See the License for the
18  *    specific language governing permissions and limitations
19  *    under the License.
20  * ====================================================================
21  */
22 
23 #include "svn_private_config.h"
24 #include "svn_hash.h"
25 #include "svn_pools.h"
26 #include "svn_sorts.h"
27 #include "private/svn_sorts_private.h"
28 #include "private/svn_string_private.h"
29 #include "private/svn_subr_private.h"
30 #include "private/svn_fspath.h"
31 
32 #include "../libsvn_fs/fs-loader.h"
33 
34 #include "low_level.h"
35 #include "util.h"
36 #include "pack.h"
37 #include "cached_data.h"
38 
39 /* Headers used to describe node-revision in the revision file. */
40 #define HEADER_ID          "id"
41 #define HEADER_NODE        "node"
42 #define HEADER_COPY        "copy"
43 #define HEADER_TYPE        "type"
44 #define HEADER_COUNT       "count"
45 #define HEADER_PROPS       "props"
46 #define HEADER_TEXT        "text"
47 #define HEADER_CPATH       "cpath"
48 #define HEADER_PRED        "pred"
49 #define HEADER_COPYFROM    "copyfrom"
50 #define HEADER_COPYROOT    "copyroot"
51 #define HEADER_MINFO_HERE  "minfo-here"
52 #define HEADER_MINFO_CNT   "minfo-cnt"
53 
54 /* Kinds that a change can be. */
55 #define ACTION_MODIFY      "modify"
56 #define ACTION_ADD         "add"
57 #define ACTION_DELETE      "delete"
58 #define ACTION_REPLACE     "replace"
59 
60 /* True and False flags. */
61 #define FLAG_TRUE          "true"
62 #define FLAG_FALSE         "false"
63 
64 /* Kinds of representation. */
65 #define REP_DELTA          "DELTA"
66 
67 /* An arbitrary maximum path length, so clients can't run us out of memory
68  * by giving us arbitrarily large paths. */
69 #define FSX_MAX_PATH_LEN 4096
70 
71 /* The 256 is an arbitrary size large enough to hold the node id and the
72  * various flags. */
73 #define MAX_CHANGE_LINE_LEN FSX_MAX_PATH_LEN + 256
74 
75 /* Convert the C string in *TEXT to a revision number and return it in *REV.
76  * Overflows, negative values other than -1 and terminating characters other
77  * than 0x20 or 0x0 will cause an error.  Set *TEXT to the first char after
78  * the initial separator or to EOS.
79  */
80 static svn_error_t *
parse_revnum(svn_revnum_t * rev,const char ** text)81 parse_revnum(svn_revnum_t *rev,
82              const char **text)
83 {
84   const char *string = *text;
85   if ((string[0] == '-') && (string[1] == '1'))
86     {
87       *rev = SVN_INVALID_REVNUM;
88       string += 2;
89     }
90   else
91     {
92       SVN_ERR(svn_revnum_parse(rev, string, &string));
93     }
94 
95   if (*string == ' ')
96     ++string;
97   else if (*string != '\0')
98     return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
99                             _("Invalid character in revision number"));
100 
101   *text = string;
102   return SVN_NO_ERROR;
103 }
104 
105 /* If ERR is not NULL, wrap it MESSAGE.  The latter must have an %ld
106  * format parameter that will be filled with REV. */
107 static svn_error_t *
wrap_footer_error(svn_error_t * err,const char * message,svn_revnum_t rev)108 wrap_footer_error(svn_error_t *err,
109                   const char *message,
110                   svn_revnum_t rev)
111 {
112   if (err)
113     return svn_error_quick_wrapf(err, message, rev);
114 
115   return SVN_NO_ERROR;
116 }
117 
118 svn_error_t *
svn_fs_x__parse_footer(apr_off_t * l2p_offset,svn_checksum_t ** l2p_checksum,apr_off_t * p2l_offset,svn_checksum_t ** p2l_checksum,svn_stringbuf_t * footer,svn_revnum_t rev,apr_off_t footer_offset,apr_pool_t * result_pool)119 svn_fs_x__parse_footer(apr_off_t *l2p_offset,
120                        svn_checksum_t **l2p_checksum,
121                        apr_off_t *p2l_offset,
122                        svn_checksum_t **p2l_checksum,
123                        svn_stringbuf_t *footer,
124                        svn_revnum_t rev,
125                        apr_off_t footer_offset,
126                        apr_pool_t *result_pool)
127 {
128   apr_int64_t val;
129   char *last_str = footer->data;
130 
131   /* Get the L2P offset. */
132   const char *str = svn_cstring_tokenize(" ", &last_str);
133   if (str == NULL)
134     return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
135                              "Invalid r%ld footer", rev);
136 
137   SVN_ERR(wrap_footer_error(svn_cstring_strtoi64(&val, str, 0,
138                                                  footer_offset - 1, 10),
139                             "Invalid L2P offset in r%ld footer",
140                             rev));
141   *l2p_offset = (apr_off_t)val;
142 
143   /* Get the L2P checksum. */
144   str = svn_cstring_tokenize(" ", &last_str);
145   if (str == NULL)
146     return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
147                              "Invalid r%ld footer", rev);
148 
149   SVN_ERR(svn_checksum_parse_hex(l2p_checksum, svn_checksum_md5, str,
150                                  result_pool));
151 
152   /* Get the P2L offset. */
153   str = svn_cstring_tokenize(" ", &last_str);
154   if (str == NULL)
155     return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
156                              "Invalid r%ld footer", rev);
157 
158   SVN_ERR(wrap_footer_error(svn_cstring_strtoi64(&val, str, 0,
159                                                  footer_offset - 1, 10),
160                             "Invalid P2L offset in r%ld footer",
161                             rev));
162   *p2l_offset = (apr_off_t)val;
163 
164   /* The P2L indes follows the L2P index */
165   if (*p2l_offset <= *l2p_offset)
166     return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
167                              "P2L offset %s must be larger than L2P offset %s"
168                              " in r%ld footer",
169                              apr_psprintf(result_pool,
170                                           "0x%" APR_UINT64_T_HEX_FMT,
171                                           (apr_uint64_t)*p2l_offset),
172                              apr_psprintf(result_pool,
173                                           "0x%" APR_UINT64_T_HEX_FMT,
174                                           (apr_uint64_t)*l2p_offset),
175                              rev);
176 
177   /* Get the P2L checksum. */
178   str = svn_cstring_tokenize(" ", &last_str);
179   if (str == NULL)
180     return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
181                              "Invalid r%ld footer", rev);
182 
183   SVN_ERR(svn_checksum_parse_hex(p2l_checksum, svn_checksum_md5, str,
184                                  result_pool));
185 
186   return SVN_NO_ERROR;
187 }
188 
189 svn_stringbuf_t *
svn_fs_x__unparse_footer(apr_off_t l2p_offset,svn_checksum_t * l2p_checksum,apr_off_t p2l_offset,svn_checksum_t * p2l_checksum,apr_pool_t * result_pool,apr_pool_t * scratch_pool)190 svn_fs_x__unparse_footer(apr_off_t l2p_offset,
191                          svn_checksum_t *l2p_checksum,
192                          apr_off_t p2l_offset,
193                          svn_checksum_t *p2l_checksum,
194                          apr_pool_t *result_pool,
195                          apr_pool_t *scratch_pool)
196 {
197   return svn_stringbuf_createf(result_pool,
198                                "%" APR_OFF_T_FMT " %s %" APR_OFF_T_FMT " %s",
199                                l2p_offset,
200                                svn_checksum_to_cstring(l2p_checksum,
201                                                        scratch_pool),
202                                p2l_offset,
203                                svn_checksum_to_cstring(p2l_checksum,
204                                                        scratch_pool));
205 }
206 
207 /* Given a revision file FILE that has been pre-positioned at the
208    beginning of a Node-Rev header block, read in that header block and
209    store it in the apr_hash_t HEADERS.  All allocations will be from
210    RESULT_POOL. */
211 static svn_error_t *
read_header_block(apr_hash_t ** headers,svn_stream_t * stream,apr_pool_t * result_pool)212 read_header_block(apr_hash_t **headers,
213                   svn_stream_t *stream,
214                   apr_pool_t *result_pool)
215 {
216   *headers = svn_hash__make(result_pool);
217 
218   while (1)
219     {
220       svn_stringbuf_t *header_str;
221       const char *name, *value;
222       apr_size_t i = 0;
223       apr_size_t name_len;
224       svn_boolean_t eof;
225 
226       SVN_ERR(svn_stream_readline(stream, &header_str, "\n", &eof,
227                                   result_pool));
228 
229       if (eof || header_str->len == 0)
230         break; /* end of header block */
231 
232       while (header_str->data[i] != ':')
233         {
234           if (header_str->data[i] == '\0')
235             return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
236                                      _("Found malformed header '%s' in "
237                                        "revision file"),
238                                      header_str->data);
239           i++;
240         }
241 
242       /* Create a 'name' string and point to it. */
243       header_str->data[i] = '\0';
244       name = header_str->data;
245       name_len = i;
246 
247       /* Check if we have enough data to parse. */
248       if (i + 2 > header_str->len)
249         {
250           /* Restore the original line for the error. */
251           header_str->data[i] = ':';
252           return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
253                                    _("Found malformed header '%s' in "
254                                      "revision file"),
255                                    header_str->data);
256         }
257 
258       /* Skip over the NULL byte and the space following it. */
259       i += 2;
260 
261       value = header_str->data + i;
262 
263       /* header_str is safely in our pool, so we can use bits of it as
264          key and value. */
265       apr_hash_set(*headers, name, name_len, value);
266     }
267 
268   return SVN_NO_ERROR;
269 }
270 
271 svn_error_t *
svn_fs_x__parse_representation(svn_fs_x__representation_t ** rep_p,svn_stringbuf_t * text,apr_pool_t * result_pool,apr_pool_t * scratch_pool)272 svn_fs_x__parse_representation(svn_fs_x__representation_t **rep_p,
273                                svn_stringbuf_t *text,
274                                apr_pool_t *result_pool,
275                                apr_pool_t *scratch_pool)
276 {
277   svn_fs_x__representation_t *rep;
278   char *str;
279   apr_int64_t val;
280   char *string = text->data;
281   svn_checksum_t *checksum;
282 
283   rep = apr_pcalloc(result_pool, sizeof(*rep));
284   *rep_p = rep;
285 
286   str = svn_cstring_tokenize(" ", &string);
287   if (str == NULL)
288     return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
289                             _("Malformed text representation offset line in node-rev"));
290 
291   SVN_ERR(svn_cstring_atoi64(&rep->id.change_set, str));
292 
293   /* while in transactions, it is legal to simply write "-1" */
294   if (rep->id.change_set == -1)
295     return SVN_NO_ERROR;
296 
297   str = svn_cstring_tokenize(" ", &string);
298   if (str == NULL)
299     {
300       if (rep->id.change_set == SVN_FS_X__INVALID_CHANGE_SET)
301         return SVN_NO_ERROR;
302 
303       return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
304                               _("Malformed text representation offset line in node-rev"));
305     }
306 
307   SVN_ERR(svn_cstring_atoi64(&val, str));
308   rep->id.number = (apr_off_t)val;
309 
310   str = svn_cstring_tokenize(" ", &string);
311   if (str == NULL)
312     return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
313                             _("Malformed text representation offset line in node-rev"));
314 
315   SVN_ERR(svn_cstring_atoi64(&val, str));
316   rep->size = (svn_filesize_t)val;
317 
318   str = svn_cstring_tokenize(" ", &string);
319   if (str == NULL)
320     return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
321                             _("Malformed text representation offset line in node-rev"));
322 
323   SVN_ERR(svn_cstring_atoi64(&val, str));
324   rep->expanded_size = (svn_filesize_t)val;
325 
326   /* Read in the MD5 hash. */
327   str = svn_cstring_tokenize(" ", &string);
328   if ((str == NULL) || (strlen(str) != (APR_MD5_DIGESTSIZE * 2)))
329     return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
330                             _("Malformed text representation offset line in node-rev"));
331 
332   SVN_ERR(svn_checksum_parse_hex(&checksum, svn_checksum_md5, str,
333                                  scratch_pool));
334   if (checksum)
335     memcpy(rep->md5_digest, checksum->digest, sizeof(rep->md5_digest));
336 
337   /* The remaining fields are only used for formats >= 4, so check that. */
338   str = svn_cstring_tokenize(" ", &string);
339   if (str == NULL)
340     return SVN_NO_ERROR;
341 
342   /* Read the SHA1 hash. */
343   if (strlen(str) != (APR_SHA1_DIGESTSIZE * 2))
344     return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
345                             _("Malformed text representation offset line in node-rev"));
346 
347   SVN_ERR(svn_checksum_parse_hex(&checksum, svn_checksum_sha1, str,
348                                  scratch_pool));
349   rep->has_sha1 = checksum != NULL;
350   if (checksum)
351     memcpy(rep->sha1_digest, checksum->digest, sizeof(rep->sha1_digest));
352 
353   return SVN_NO_ERROR;
354 }
355 
356 /* Wrap read_rep_offsets_body(), extracting its TXN_ID from our NODEREV_ID,
357    and adding an error message. */
358 static svn_error_t *
read_rep_offsets(svn_fs_x__representation_t ** rep_p,char * string,const svn_fs_x__id_t * noderev_id,apr_pool_t * result_pool,apr_pool_t * scratch_pool)359 read_rep_offsets(svn_fs_x__representation_t **rep_p,
360                  char *string,
361                  const svn_fs_x__id_t *noderev_id,
362                  apr_pool_t *result_pool,
363                  apr_pool_t *scratch_pool)
364 {
365   svn_error_t *err
366     = svn_fs_x__parse_representation(rep_p,
367                                      svn_stringbuf_create_wrap(string,
368                                                                scratch_pool),
369                                      result_pool,
370                                      scratch_pool);
371   if (err)
372     {
373       const svn_string_t *id_unparsed;
374       const char *where;
375 
376       id_unparsed = svn_fs_x__id_unparse(noderev_id, scratch_pool);
377       where = apr_psprintf(scratch_pool,
378                            _("While reading representation offsets "
379                              "for node-revision '%s':"),
380                            id_unparsed->data);
381 
382       return svn_error_quick_wrap(err, where);
383     }
384 
385   return SVN_NO_ERROR;
386 }
387 
388 /* If PATH needs to be escaped, return an escaped version of it, allocated
389  * from RESULT_POOL. Otherwise, return PATH directly. */
390 static const char *
auto_escape_path(const char * path,apr_pool_t * result_pool)391 auto_escape_path(const char *path,
392                  apr_pool_t *result_pool)
393 {
394   apr_size_t len = strlen(path);
395   apr_size_t i;
396   const char esc = '\x1b';
397 
398   for (i = 0; i < len; ++i)
399     if (path[i] < ' ')
400       {
401         svn_stringbuf_t *escaped = svn_stringbuf_create_ensure(2 * len,
402                                                                result_pool);
403         for (i = 0; i < len; ++i)
404           if (path[i] < ' ')
405             {
406               svn_stringbuf_appendbyte(escaped, esc);
407               svn_stringbuf_appendbyte(escaped, path[i] + 'A' - 1);
408             }
409           else
410             {
411               svn_stringbuf_appendbyte(escaped, path[i]);
412             }
413 
414         return escaped->data;
415       }
416 
417    return path;
418 }
419 
420 /* If PATH has been escaped, return the un-escaped version of it, allocated
421  * from RESULT_POOL. Otherwise, return PATH directly. */
422 static const char *
auto_unescape_path(const char * path,apr_pool_t * result_pool)423 auto_unescape_path(const char *path,
424                    apr_pool_t *result_pool)
425 {
426   const char esc = '\x1b';
427   if (strchr(path, esc))
428     {
429       apr_size_t len = strlen(path);
430       apr_size_t i;
431 
432       svn_stringbuf_t *unescaped = svn_stringbuf_create_ensure(len,
433                                                                result_pool);
434       for (i = 0; i < len; ++i)
435         if (path[i] == esc)
436           svn_stringbuf_appendbyte(unescaped, path[++i] + 1 - 'A');
437         else
438           svn_stringbuf_appendbyte(unescaped, path[i]);
439 
440       return unescaped->data;
441     }
442 
443    return path;
444 }
445 
446 /* Find entry HEADER_NAME in HEADERS and parse its value into *ID. */
447 static svn_error_t *
read_id_part(svn_fs_x__id_t * id,apr_hash_t * headers,const char * header_name)448 read_id_part(svn_fs_x__id_t *id,
449              apr_hash_t *headers,
450              const char *header_name)
451 {
452   const char *value = svn_hash_gets(headers, header_name);
453   if (value == NULL)
454     return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
455                              _("Missing %s field in node-rev"),
456                              header_name);
457 
458   SVN_ERR(svn_fs_x__id_parse(id, value));
459   return SVN_NO_ERROR;
460 }
461 
462 svn_error_t *
svn_fs_x__read_noderev(svn_fs_x__noderev_t ** noderev_p,svn_stream_t * stream,apr_pool_t * result_pool,apr_pool_t * scratch_pool)463 svn_fs_x__read_noderev(svn_fs_x__noderev_t **noderev_p,
464                        svn_stream_t *stream,
465                        apr_pool_t *result_pool,
466                        apr_pool_t *scratch_pool)
467 {
468   apr_hash_t *headers;
469   svn_fs_x__noderev_t *noderev;
470   char *value;
471   const char *noderev_id;
472 
473   SVN_ERR(read_header_block(&headers, stream, scratch_pool));
474   SVN_ERR(svn_stream_close(stream));
475 
476   noderev = apr_pcalloc(result_pool, sizeof(*noderev));
477 
478   /* for error messages later */
479   noderev_id = svn_hash_gets(headers, HEADER_ID);
480 
481   /* Read the node-rev id. */
482   SVN_ERR(read_id_part(&noderev->noderev_id, headers, HEADER_ID));
483   SVN_ERR(read_id_part(&noderev->node_id, headers, HEADER_NODE));
484   SVN_ERR(read_id_part(&noderev->copy_id, headers, HEADER_COPY));
485 
486   /* Read the type. */
487   value = svn_hash_gets(headers, HEADER_TYPE);
488 
489   if ((value == NULL) ||
490       (   strcmp(value, SVN_FS_X__KIND_FILE)
491        && strcmp(value, SVN_FS_X__KIND_DIR)))
492     /* ### s/kind/type/ */
493     return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
494                              _("Missing kind field in node-rev '%s'"),
495                              noderev_id);
496 
497   noderev->kind = (strcmp(value, SVN_FS_X__KIND_FILE) == 0)
498                 ? svn_node_file
499                 : svn_node_dir;
500 
501   /* Read the 'count' field. */
502   value = svn_hash_gets(headers, HEADER_COUNT);
503   if (value)
504     SVN_ERR(svn_cstring_atoi(&noderev->predecessor_count, value));
505   else
506     noderev->predecessor_count = 0;
507 
508   /* Get the properties location. */
509   value = svn_hash_gets(headers, HEADER_PROPS);
510   if (value)
511     {
512       SVN_ERR(read_rep_offsets(&noderev->prop_rep, value,
513                                &noderev->noderev_id, result_pool,
514                                scratch_pool));
515     }
516 
517   /* Get the data location. */
518   value = svn_hash_gets(headers, HEADER_TEXT);
519   if (value)
520     {
521       SVN_ERR(read_rep_offsets(&noderev->data_rep, value,
522                                &noderev->noderev_id, result_pool,
523                                scratch_pool));
524     }
525 
526   /* Get the created path. */
527   value = svn_hash_gets(headers, HEADER_CPATH);
528   if (value == NULL)
529     {
530       return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
531                                _("Missing cpath field in node-rev '%s'"),
532                                noderev_id);
533     }
534   else
535     {
536       if (!svn_fspath__is_canonical(value))
537         return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
538                             _("Non-canonical cpath field in node-rev '%s'"),
539                             noderev_id);
540 
541       noderev->created_path = auto_unescape_path(apr_pstrdup(result_pool,
542                                                               value),
543                                                  result_pool);
544     }
545 
546   /* Get the predecessor ID. */
547   value = svn_hash_gets(headers, HEADER_PRED);
548   if (value)
549     SVN_ERR(svn_fs_x__id_parse(&noderev->predecessor_id, value));
550   else
551     svn_fs_x__id_reset(&noderev->predecessor_id);
552 
553   /* Get the copyroot. */
554   value = svn_hash_gets(headers, HEADER_COPYROOT);
555   if (value == NULL)
556     {
557       noderev->copyroot_path = noderev->created_path;
558       noderev->copyroot_rev
559         = svn_fs_x__get_revnum(noderev->noderev_id.change_set);
560     }
561   else
562     {
563       SVN_ERR(parse_revnum(&noderev->copyroot_rev, (const char **)&value));
564 
565       if (!svn_fspath__is_canonical(value))
566         return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
567                                  _("Malformed copyroot line in node-rev '%s'"),
568                                  noderev_id);
569       noderev->copyroot_path = auto_unescape_path(apr_pstrdup(result_pool,
570                                                               value),
571                                                   result_pool);
572     }
573 
574   /* Get the copyfrom. */
575   value = svn_hash_gets(headers, HEADER_COPYFROM);
576   if (value == NULL)
577     {
578       noderev->copyfrom_path = NULL;
579       noderev->copyfrom_rev = SVN_INVALID_REVNUM;
580     }
581   else
582     {
583       SVN_ERR(parse_revnum(&noderev->copyfrom_rev, (const char **)&value));
584 
585       if (*value == 0)
586         return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
587                                  _("Malformed copyfrom line in node-rev '%s'"),
588                                  noderev_id);
589       noderev->copyfrom_path = auto_unescape_path(apr_pstrdup(result_pool,
590                                                               value),
591                                                   result_pool);
592     }
593 
594   /* Get the mergeinfo count. */
595   value = svn_hash_gets(headers, HEADER_MINFO_CNT);
596   if (value)
597     SVN_ERR(svn_cstring_atoi64(&noderev->mergeinfo_count, value));
598   else
599     noderev->mergeinfo_count = 0;
600 
601   /* Get whether *this* node has mergeinfo. */
602   value = svn_hash_gets(headers, HEADER_MINFO_HERE);
603   noderev->has_mergeinfo = (value != NULL);
604 
605   *noderev_p = noderev;
606 
607   return SVN_NO_ERROR;
608 }
609 
610 /* Return a textual representation of the DIGEST of given KIND.
611  * If IS_NULL is TRUE, no digest is available.
612  * Allocate the result in RESULT_POOL.
613  */
614 static const char *
format_digest(const unsigned char * digest,svn_checksum_kind_t kind,svn_boolean_t is_null,apr_pool_t * result_pool)615 format_digest(const unsigned char *digest,
616               svn_checksum_kind_t kind,
617               svn_boolean_t is_null,
618               apr_pool_t *result_pool)
619 {
620   svn_checksum_t checksum;
621   checksum.digest = digest;
622   checksum.kind = kind;
623 
624   if (is_null)
625     return "(null)";
626 
627   return svn_checksum_to_cstring_display(&checksum, result_pool);
628 }
629 
630 svn_stringbuf_t *
svn_fs_x__unparse_representation(svn_fs_x__representation_t * rep,svn_boolean_t mutable_rep_truncated,apr_pool_t * result_pool,apr_pool_t * scratch_pool)631 svn_fs_x__unparse_representation(svn_fs_x__representation_t *rep,
632                                  svn_boolean_t mutable_rep_truncated,
633                                  apr_pool_t *result_pool,
634                                  apr_pool_t *scratch_pool)
635 {
636   if (!rep->has_sha1)
637     return svn_stringbuf_createf
638             (result_pool,
639              "%" APR_INT64_T_FMT " %" APR_UINT64_T_FMT " %" SVN_FILESIZE_T_FMT
640              " %" SVN_FILESIZE_T_FMT " %s",
641              rep->id.change_set, rep->id.number, rep->size,
642              rep->expanded_size,
643              format_digest(rep->md5_digest, svn_checksum_md5, FALSE,
644                            scratch_pool));
645 
646   return svn_stringbuf_createf
647           (result_pool,
648            "%" APR_INT64_T_FMT " %" APR_UINT64_T_FMT " %" SVN_FILESIZE_T_FMT
649            " %" SVN_FILESIZE_T_FMT " %s %s",
650            rep->id.change_set, rep->id.number, rep->size,
651            rep->expanded_size,
652            format_digest(rep->md5_digest, svn_checksum_md5,
653                          FALSE, scratch_pool),
654            format_digest(rep->sha1_digest, svn_checksum_sha1,
655                          !rep->has_sha1, scratch_pool));
656 }
657 
658 
659 svn_error_t *
svn_fs_x__write_noderev(svn_stream_t * outfile,svn_fs_x__noderev_t * noderev,apr_pool_t * scratch_pool)660 svn_fs_x__write_noderev(svn_stream_t *outfile,
661                         svn_fs_x__noderev_t *noderev,
662                         apr_pool_t *scratch_pool)
663 {
664   svn_string_t *str_id;
665 
666   str_id = svn_fs_x__id_unparse(&noderev->noderev_id, scratch_pool);
667   SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_ID ": %s\n",
668                             str_id->data));
669   str_id = svn_fs_x__id_unparse(&noderev->node_id, scratch_pool);
670   SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_NODE ": %s\n",
671                             str_id->data));
672   str_id = svn_fs_x__id_unparse(&noderev->copy_id, scratch_pool);
673   SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_COPY ": %s\n",
674                             str_id->data));
675 
676   SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_TYPE ": %s\n",
677                             (noderev->kind == svn_node_file) ?
678                             SVN_FS_X__KIND_FILE : SVN_FS_X__KIND_DIR));
679 
680   if (svn_fs_x__id_used(&noderev->predecessor_id))
681     SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_PRED ": %s\n",
682                               svn_fs_x__id_unparse(&noderev->predecessor_id,
683                                                    scratch_pool)->data));
684 
685   SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_COUNT ": %d\n",
686                             noderev->predecessor_count));
687 
688   if (noderev->data_rep)
689     SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_TEXT ": %s\n",
690                               svn_fs_x__unparse_representation
691                                 (noderev->data_rep,
692                                  noderev->kind == svn_node_dir,
693                                  scratch_pool, scratch_pool)->data));
694 
695   if (noderev->prop_rep)
696     SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_PROPS ": %s\n",
697                               svn_fs_x__unparse_representation
698                                 (noderev->prop_rep,
699                                  TRUE, scratch_pool, scratch_pool)->data));
700 
701   SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_CPATH ": %s\n",
702                             auto_escape_path(noderev->created_path,
703                                              scratch_pool)));
704 
705   if (noderev->copyfrom_path)
706     SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_COPYFROM ": %ld"
707                               " %s\n",
708                               noderev->copyfrom_rev,
709                               auto_escape_path(noderev->copyfrom_path,
710                                                scratch_pool)));
711 
712   if (   (   noderev->copyroot_rev
713            != svn_fs_x__get_revnum(noderev->noderev_id.change_set))
714       || (strcmp(noderev->copyroot_path, noderev->created_path) != 0))
715     SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_COPYROOT ": %ld"
716                               " %s\n",
717                               noderev->copyroot_rev,
718                               auto_escape_path(noderev->copyroot_path,
719                                                scratch_pool)));
720 
721   if (noderev->mergeinfo_count > 0)
722     SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_MINFO_CNT ": %"
723                               APR_INT64_T_FMT "\n",
724                               noderev->mergeinfo_count));
725 
726   if (noderev->has_mergeinfo)
727     SVN_ERR(svn_stream_puts(outfile, HEADER_MINFO_HERE ": y\n"));
728 
729   return svn_stream_puts(outfile, "\n");
730 }
731 
732 svn_error_t *
svn_fs_x__read_rep_header(svn_fs_x__rep_header_t ** header,svn_stream_t * stream,apr_pool_t * result_pool,apr_pool_t * scratch_pool)733 svn_fs_x__read_rep_header(svn_fs_x__rep_header_t **header,
734                           svn_stream_t *stream,
735                           apr_pool_t *result_pool,
736                           apr_pool_t *scratch_pool)
737 {
738   svn_stringbuf_t *buffer;
739   char *str, *last_str;
740   apr_int64_t val;
741   svn_boolean_t eol = FALSE;
742 
743   SVN_ERR(svn_stream_readline(stream, &buffer, "\n", &eol, scratch_pool));
744 
745   *header = apr_pcalloc(result_pool, sizeof(**header));
746   (*header)->header_size = buffer->len + 1;
747   if (strcmp(buffer->data, REP_DELTA) == 0)
748     {
749       /* This is a delta against the empty stream. */
750       (*header)->type = svn_fs_x__rep_self_delta;
751       return SVN_NO_ERROR;
752     }
753 
754   (*header)->type = svn_fs_x__rep_delta;
755 
756   /* We have hopefully a DELTA vs. a non-empty base revision. */
757   last_str = buffer->data;
758   str = svn_cstring_tokenize(" ", &last_str);
759   if (! str || (strcmp(str, REP_DELTA) != 0))
760     goto error;
761 
762   SVN_ERR(parse_revnum(&(*header)->base_revision, (const char **)&last_str));
763 
764   str = svn_cstring_tokenize(" ", &last_str);
765   if (! str)
766     goto error;
767   SVN_ERR(svn_cstring_atoi64(&val, str));
768   (*header)->base_item_index = (apr_off_t)val;
769 
770   str = svn_cstring_tokenize(" ", &last_str);
771   if (! str)
772     goto error;
773   SVN_ERR(svn_cstring_atoi64(&val, str));
774   (*header)->base_length = (svn_filesize_t)val;
775 
776   return SVN_NO_ERROR;
777 
778  error:
779   return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
780                            _("Malformed representation header"));
781 }
782 
783 svn_error_t *
svn_fs_x__write_rep_header(svn_fs_x__rep_header_t * header,svn_stream_t * stream,apr_pool_t * scratch_pool)784 svn_fs_x__write_rep_header(svn_fs_x__rep_header_t *header,
785                            svn_stream_t *stream,
786                            apr_pool_t *scratch_pool)
787 {
788   const char *text;
789 
790   switch (header->type)
791     {
792       case svn_fs_x__rep_self_delta:
793         text = REP_DELTA "\n";
794         break;
795 
796       default:
797         text = apr_psprintf(scratch_pool, REP_DELTA " %ld %" APR_OFF_T_FMT
798                                           " %" SVN_FILESIZE_T_FMT "\n",
799                             header->base_revision, header->base_item_index,
800                             header->base_length);
801     }
802 
803   return svn_error_trace(svn_stream_puts(stream, text));
804 }
805 
806 /* Read the next entry in the changes record from file FILE and store
807    the resulting change in *CHANGE_P.  If there is no next record,
808    store NULL there.  Perform all allocations from POOL. */
809 static svn_error_t *
read_change(svn_fs_x__change_t ** change_p,svn_stream_t * stream,apr_pool_t * result_pool,apr_pool_t * scratch_pool)810 read_change(svn_fs_x__change_t **change_p,
811             svn_stream_t *stream,
812             apr_pool_t *result_pool,
813             apr_pool_t *scratch_pool)
814 {
815   svn_stringbuf_t *line;
816   svn_boolean_t eof = TRUE;
817   svn_fs_x__change_t *change;
818   char *str, *last_str, *kind_str;
819 
820   /* Default return value. */
821   *change_p = NULL;
822 
823   SVN_ERR(svn_stream_readline(stream, &line, "\n", &eof, scratch_pool));
824 
825   /* Check for a blank line. */
826   if (eof || (line->len == 0))
827     return SVN_NO_ERROR;
828 
829   change = apr_pcalloc(result_pool, sizeof(*change));
830   last_str = line->data;
831 
832   /* Get the change type. */
833   str = svn_cstring_tokenize(" ", &last_str);
834   if (str == NULL)
835     return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
836                             _("Invalid changes line in rev-file"));
837 
838   /* Don't bother to check the format number before looking for
839    * node-kinds: just read them if you find them. */
840   change->node_kind = svn_node_unknown;
841   kind_str = strchr(str, '-');
842   if (kind_str)
843     {
844       /* Cap off the end of "str" (the action). */
845       *kind_str = '\0';
846       kind_str++;
847       if (strcmp(kind_str, SVN_FS_X__KIND_FILE) == 0)
848         change->node_kind = svn_node_file;
849       else if (strcmp(kind_str, SVN_FS_X__KIND_DIR) == 0)
850         change->node_kind = svn_node_dir;
851       else
852         return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
853                                 _("Invalid changes line in rev-file"));
854     }
855 
856   if (strcmp(str, ACTION_MODIFY) == 0)
857     {
858       change->change_kind = svn_fs_path_change_modify;
859     }
860   else if (strcmp(str, ACTION_ADD) == 0)
861     {
862       change->change_kind = svn_fs_path_change_add;
863     }
864   else if (strcmp(str, ACTION_DELETE) == 0)
865     {
866       change->change_kind = svn_fs_path_change_delete;
867     }
868   else if (strcmp(str, ACTION_REPLACE) == 0)
869     {
870       change->change_kind = svn_fs_path_change_replace;
871     }
872   else
873     {
874       return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
875                               _("Invalid change kind in rev file"));
876     }
877 
878   /* Get the text-mod flag. */
879   str = svn_cstring_tokenize(" ", &last_str);
880   if (str == NULL)
881     return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
882                             _("Invalid changes line in rev-file"));
883 
884   if (strcmp(str, FLAG_TRUE) == 0)
885     {
886       change->text_mod = TRUE;
887     }
888   else if (strcmp(str, FLAG_FALSE) == 0)
889     {
890       change->text_mod = FALSE;
891     }
892   else
893     {
894       return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
895                               _("Invalid text-mod flag in rev-file"));
896     }
897 
898   /* Get the prop-mod flag. */
899   str = svn_cstring_tokenize(" ", &last_str);
900   if (str == NULL)
901     return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
902                             _("Invalid changes line in rev-file"));
903 
904   if (strcmp(str, FLAG_TRUE) == 0)
905     {
906       change->prop_mod = TRUE;
907     }
908   else if (strcmp(str, FLAG_FALSE) == 0)
909     {
910       change->prop_mod = FALSE;
911     }
912   else
913     {
914       return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
915                               _("Invalid prop-mod flag in rev-file"));
916     }
917 
918   /* Get the mergeinfo-mod flag. */
919   str = svn_cstring_tokenize(" ", &last_str);
920   if (str == NULL)
921     return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
922                             _("Invalid changes line in rev-file"));
923 
924   if (strcmp(str, FLAG_TRUE) == 0)
925     {
926       change->mergeinfo_mod = svn_tristate_true;
927     }
928   else if (strcmp(str, FLAG_FALSE) == 0)
929     {
930       change->mergeinfo_mod = svn_tristate_false;
931     }
932   else
933     {
934       return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
935                               _("Invalid mergeinfo-mod flag in rev-file"));
936     }
937 
938   /* Get the changed path. */
939   if (!svn_fspath__is_canonical(last_str))
940     return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
941                             _("Invalid path in changes line"));
942 
943   change->path.data = auto_unescape_path(apr_pstrmemdup(result_pool,
944                                                         last_str,
945                                                         strlen(last_str)),
946                                          result_pool);
947   change->path.len = strlen(change->path.data);
948 
949   /* Read the next line, the copyfrom line. */
950   SVN_ERR(svn_stream_readline(stream, &line, "\n", &eof, result_pool));
951   change->copyfrom_known = TRUE;
952   if (eof || line->len == 0)
953     {
954       change->copyfrom_rev = SVN_INVALID_REVNUM;
955       change->copyfrom_path = NULL;
956     }
957   else
958     {
959       last_str = line->data;
960       SVN_ERR(parse_revnum(&change->copyfrom_rev, (const char **)&last_str));
961 
962       if (!svn_fspath__is_canonical(last_str))
963         return svn_error_create(SVN_ERR_FS_CORRUPT, NULL,
964                                 _("Invalid copy-from path in changes line"));
965 
966       change->copyfrom_path = auto_unescape_path(last_str, result_pool);
967     }
968 
969   *change_p = change;
970 
971   return SVN_NO_ERROR;
972 }
973 
974 svn_error_t *
svn_fs_x__read_changes(apr_array_header_t ** changes,svn_stream_t * stream,int max_count,apr_pool_t * result_pool,apr_pool_t * scratch_pool)975 svn_fs_x__read_changes(apr_array_header_t **changes,
976                        svn_stream_t *stream,
977                        int max_count,
978                        apr_pool_t *result_pool,
979                        apr_pool_t *scratch_pool)
980 {
981   apr_pool_t *iterpool;
982 
983   /* Pre-allocate enough room for most change lists.
984      (will be auto-expanded as necessary).
985 
986      Chose the default to just below 2^N such that the doubling reallocs
987      will request roughly 2^M bytes from the OS without exceeding the
988      respective two-power by just a few bytes (leaves room array and APR
989      node overhead for large enough M).
990    */
991   *changes = apr_array_make(result_pool, 63, sizeof(svn_fs_x__change_t *));
992 
993   iterpool = svn_pool_create(scratch_pool);
994   for (; max_count > 0; --max_count)
995     {
996       svn_fs_x__change_t *change;
997       svn_pool_clear(iterpool);
998       SVN_ERR(read_change(&change, stream, result_pool, iterpool));
999       if (!change)
1000         break;
1001 
1002       APR_ARRAY_PUSH(*changes, svn_fs_x__change_t*) = change;
1003     }
1004   svn_pool_destroy(iterpool);
1005 
1006   return SVN_NO_ERROR;
1007 }
1008 
1009 svn_error_t *
svn_fs_x__read_changes_incrementally(svn_stream_t * stream,svn_fs_x__change_receiver_t change_receiver,void * change_receiver_baton,apr_pool_t * scratch_pool)1010 svn_fs_x__read_changes_incrementally(svn_stream_t *stream,
1011                                      svn_fs_x__change_receiver_t
1012                                        change_receiver,
1013                                      void *change_receiver_baton,
1014                                      apr_pool_t *scratch_pool)
1015 {
1016   svn_fs_x__change_t *change;
1017   apr_pool_t *iterpool;
1018 
1019   iterpool = svn_pool_create(scratch_pool);
1020   do
1021     {
1022       svn_pool_clear(iterpool);
1023 
1024       SVN_ERR(read_change(&change, stream, iterpool, iterpool));
1025       if (change)
1026         SVN_ERR(change_receiver(change_receiver_baton, change, iterpool));
1027     }
1028   while (change);
1029   svn_pool_destroy(iterpool);
1030 
1031   return SVN_NO_ERROR;
1032 }
1033 
1034 /* Write a single change entry, path PATH, change CHANGE, to STREAM.
1035 
1036    All temporary allocations are in SCRATCH_POOL. */
1037 static svn_error_t *
write_change_entry(svn_stream_t * stream,svn_fs_x__change_t * change,apr_pool_t * scratch_pool)1038 write_change_entry(svn_stream_t *stream,
1039                    svn_fs_x__change_t *change,
1040                    apr_pool_t *scratch_pool)
1041 {
1042   const char *change_string = NULL;
1043   const char *kind_string = "";
1044   svn_stringbuf_t *buf;
1045   apr_size_t len;
1046 
1047   switch (change->change_kind)
1048     {
1049     case svn_fs_path_change_modify:
1050       change_string = ACTION_MODIFY;
1051       break;
1052     case svn_fs_path_change_add:
1053       change_string = ACTION_ADD;
1054       break;
1055     case svn_fs_path_change_delete:
1056       change_string = ACTION_DELETE;
1057       break;
1058     case svn_fs_path_change_replace:
1059       change_string = ACTION_REPLACE;
1060       break;
1061     default:
1062       return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL,
1063                                _("Invalid change type %d"),
1064                                change->change_kind);
1065     }
1066 
1067   SVN_ERR_ASSERT(change->node_kind == svn_node_dir
1068                  || change->node_kind == svn_node_file);
1069   kind_string = apr_psprintf(scratch_pool, "-%s",
1070                              change->node_kind == svn_node_dir
1071                              ? SVN_FS_X__KIND_DIR
1072                              : SVN_FS_X__KIND_FILE);
1073 
1074   buf = svn_stringbuf_createf(scratch_pool, "%s%s %s %s %s %s\n",
1075                               change_string, kind_string,
1076                               change->text_mod ? FLAG_TRUE : FLAG_FALSE,
1077                               change->prop_mod ? FLAG_TRUE : FLAG_FALSE,
1078                               change->mergeinfo_mod == svn_tristate_true
1079                                                ? FLAG_TRUE : FLAG_FALSE,
1080                               auto_escape_path(change->path.data, scratch_pool));
1081 
1082   if (SVN_IS_VALID_REVNUM(change->copyfrom_rev))
1083     {
1084       svn_stringbuf_appendcstr(buf, apr_psprintf(scratch_pool, "%ld %s",
1085                                change->copyfrom_rev,
1086                                auto_escape_path(change->copyfrom_path,
1087                                                 scratch_pool)));
1088     }
1089 
1090   svn_stringbuf_appendbyte(buf, '\n');
1091 
1092   /* Write all change info in one write call. */
1093   len = buf->len;
1094   return svn_error_trace(svn_stream_write(stream, buf->data, &len));
1095 }
1096 
1097 svn_error_t *
svn_fs_x__write_changes(svn_stream_t * stream,svn_fs_t * fs,apr_hash_t * changes,svn_boolean_t terminate_list,apr_pool_t * scratch_pool)1098 svn_fs_x__write_changes(svn_stream_t *stream,
1099                         svn_fs_t *fs,
1100                         apr_hash_t *changes,
1101                         svn_boolean_t terminate_list,
1102                         apr_pool_t *scratch_pool)
1103 {
1104   apr_pool_t *iterpool = svn_pool_create(scratch_pool);
1105   apr_array_header_t *sorted_changed_paths;
1106   int i;
1107 
1108   /* For the sake of the repository administrator sort the changes so
1109      that the final file is deterministic and repeatable, however the
1110      rest of the FSX code doesn't require any particular order here.
1111 
1112      Also, this sorting is only effective in writing all entries with
1113      a single call as write_final_changed_path_info() does.  For the
1114      list being written incrementally during transaction, we actually
1115      *must not* change the order of entries from different calls.
1116    */
1117   sorted_changed_paths = svn_sort__hash(changes,
1118                                         svn_sort_compare_items_lexically,
1119                                         scratch_pool);
1120 
1121   /* Write all items to disk in the new order. */
1122   for (i = 0; i < sorted_changed_paths->nelts; ++i)
1123     {
1124       svn_fs_x__change_t *change;
1125 
1126       svn_pool_clear(iterpool);
1127       change = APR_ARRAY_IDX(sorted_changed_paths, i, svn_sort__item_t).value;
1128 
1129       /* Write out the new entry into the final rev-file. */
1130       SVN_ERR(write_change_entry(stream, change, iterpool));
1131     }
1132 
1133   if (terminate_list)
1134     SVN_ERR(svn_stream_puts(stream, "\n"));
1135 
1136   svn_pool_destroy(iterpool);
1137 
1138   return SVN_NO_ERROR;
1139 }
1140 
1141 svn_error_t *
svn_fs_x__parse_properties(apr_hash_t ** properties,const svn_string_t * content,apr_pool_t * result_pool)1142 svn_fs_x__parse_properties(apr_hash_t **properties,
1143                            const svn_string_t *content,
1144                            apr_pool_t *result_pool)
1145 {
1146   const apr_byte_t *p = (const apr_byte_t *)content->data;
1147   const apr_byte_t *end = p + content->len;
1148   apr_uint64_t count;
1149 
1150   *properties = apr_hash_make(result_pool);
1151 
1152   /* Extract the number of properties we are expected to read. */
1153   p = svn__decode_uint(&count, p, end);
1154 
1155   /* Read all the properties we find.
1156      Because prop-name and prop-value are nicely NUL-terminated
1157      sub-strings of CONTENT, we can simply reference them there.
1158      I.e. there is no need to copy them around.
1159    */
1160   while (p < end)
1161     {
1162       apr_uint64_t value_len;
1163       svn_string_t *value;
1164 
1165       const char *key = (const char *)p;
1166 
1167       /* Note that this may never overflow / segfault because
1168          CONTENT itself is NUL-terminated. */
1169       apr_size_t key_len = strlen(key);
1170       p += key_len + 1;
1171       if (key[key_len])
1172         return svn_error_createf(SVN_ERR_FS_CORRUPT_PROPLIST, NULL,
1173                                  "Property name not NUL terminated");
1174 
1175       if (p >= end)
1176         return svn_error_createf(SVN_ERR_FS_CORRUPT_PROPLIST, NULL,
1177                                  "Property value missing");
1178       p = svn__decode_uint(&value_len, p, end);
1179       if (value_len >= (end - p))
1180         return svn_error_createf(SVN_ERR_FS_CORRUPT_PROPLIST, NULL,
1181                                  "Property value too long");
1182 
1183       value = apr_pcalloc(result_pool, sizeof(*value));
1184       value->data = (const char *)p;
1185       value->len = (apr_size_t)value_len;
1186       if (p[value->len])
1187         return svn_error_createf(SVN_ERR_FS_CORRUPT_PROPLIST, NULL,
1188                                  "Property value not NUL terminated");
1189 
1190       p += value->len + 1;
1191 
1192       apr_hash_set(*properties, key, key_len, value);
1193     }
1194 
1195   /* Check that we read the expected number of properties. */
1196   if ((apr_uint64_t)apr_hash_count(*properties) != count)
1197     return svn_error_createf(SVN_ERR_FS_CORRUPT_PROPLIST, NULL,
1198                              "Property count mismatch");
1199 
1200   return SVN_NO_ERROR;
1201 }
1202 
1203 svn_error_t *
svn_fs_x__write_properties(svn_stream_t * stream,apr_hash_t * proplist,apr_pool_t * scratch_pool)1204 svn_fs_x__write_properties(svn_stream_t *stream,
1205                            apr_hash_t *proplist,
1206                            apr_pool_t *scratch_pool)
1207 {
1208   apr_byte_t buffer[SVN__MAX_ENCODED_UINT_LEN];
1209   apr_size_t len;
1210   apr_hash_index_t *hi;
1211 
1212   /* Write the number of properties in this list. */
1213   len = svn__encode_uint(buffer, apr_hash_count(proplist)) - buffer;
1214   SVN_ERR(svn_stream_write(stream, (const char *)buffer, &len));
1215 
1216   /* Serialize each property as follows:
1217      <Prop-name> <NUL>
1218      <Value-len> <Prop-value> <NUL>
1219    */
1220   for (hi = apr_hash_first(scratch_pool, proplist);
1221        hi;
1222        hi = apr_hash_next(hi))
1223     {
1224       const char *key;
1225       apr_size_t key_len;
1226       svn_string_t *value;
1227       apr_hash_this(hi, (const void **)&key, (apr_ssize_t *)&key_len,
1228                     (void **)&value);
1229 
1230       /* Include the terminating NUL. */
1231       ++key_len;
1232       SVN_ERR(svn_stream_write(stream, key, &key_len));
1233 
1234       len = svn__encode_uint(buffer, value->len) - buffer;
1235       SVN_ERR(svn_stream_write(stream, (const char *)buffer, &len));
1236       SVN_ERR(svn_stream_write(stream, value->data, &value->len));
1237 
1238       /* Terminate with NUL. */
1239       len = 1;
1240       SVN_ERR(svn_stream_write(stream, "", &len));
1241     }
1242 
1243   return SVN_NO_ERROR;
1244 }
1245