low_level.c revision 362181
1/* low_level.c --- low level r/w access to fs_fs 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 36/* Headers used to describe node-revision in the revision file. */ 37#define HEADER_ID "id" 38#define HEADER_TYPE "type" 39#define HEADER_COUNT "count" 40#define HEADER_PROPS "props" 41#define HEADER_TEXT "text" 42#define HEADER_CPATH "cpath" 43#define HEADER_PRED "pred" 44#define HEADER_COPYFROM "copyfrom" 45#define HEADER_COPYROOT "copyroot" 46#define HEADER_FRESHTXNRT "is-fresh-txn-root" 47#define HEADER_MINFO_HERE "minfo-here" 48#define HEADER_MINFO_CNT "minfo-cnt" 49 50/* Kinds that a change can be. */ 51#define ACTION_MODIFY "modify" 52#define ACTION_ADD "add" 53#define ACTION_DELETE "delete" 54#define ACTION_REPLACE "replace" 55#define ACTION_RESET "reset" 56 57/* True and False flags. */ 58#define FLAG_TRUE "true" 59#define FLAG_FALSE "false" 60 61/* Kinds of representation. */ 62#define REP_PLAIN "PLAIN" 63#define REP_DELTA "DELTA" 64 65/* An arbitrary maximum path length, so clients can't run us out of memory 66 * by giving us arbitrarily large paths. */ 67#define FSFS_MAX_PATH_LEN 4096 68 69/* The 256 is an arbitrary size large enough to hold the node id and the 70 * various flags. */ 71#define MAX_CHANGE_LINE_LEN FSFS_MAX_PATH_LEN + 256 72 73/* Convert the C string in *TEXT to a revision number and return it in *REV. 74 * Overflows, negative values other than -1 and terminating characters other 75 * than 0x20 or 0x0 will cause an error. Set *TEXT to the first char after 76 * the initial separator or to EOS. 77 */ 78static svn_error_t * 79parse_revnum(svn_revnum_t *rev, 80 const char **text) 81{ 82 const char *string = *text; 83 if ((string[0] == '-') && (string[1] == '1')) 84 { 85 *rev = SVN_INVALID_REVNUM; 86 string += 2; 87 } 88 else 89 { 90 SVN_ERR(svn_revnum_parse(rev, string, &string)); 91 } 92 93 if (*string == ' ') 94 ++string; 95 else if (*string != '\0') 96 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 97 _("Invalid character in revision number")); 98 99 *text = string; 100 return SVN_NO_ERROR; 101} 102 103svn_error_t * 104svn_fs_fs__parse_revision_trailer(apr_off_t *root_offset, 105 apr_off_t *changes_offset, 106 svn_stringbuf_t *trailer, 107 svn_revnum_t rev) 108{ 109 int i, num_bytes; 110 const char *str; 111 112 /* This cast should be safe since the maximum amount read, 64, will 113 never be bigger than the size of an int. */ 114 num_bytes = (int) trailer->len; 115 116 /* The last byte should be a newline. */ 117 if (trailer->len == 0 || trailer->data[trailer->len - 1] != '\n') 118 { 119 return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, 120 _("Revision file (r%ld) lacks trailing newline"), 121 rev); 122 } 123 124 /* Look for the next previous newline. */ 125 for (i = num_bytes - 2; i >= 0; i--) 126 { 127 if (trailer->data[i] == '\n') 128 break; 129 } 130 131 if (i < 0) 132 { 133 return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, 134 _("Final line in revision file (r%ld) longer " 135 "than 64 characters"), 136 rev); 137 } 138 139 i++; 140 str = &trailer->data[i]; 141 142 /* find the next space */ 143 for ( ; i < (num_bytes - 2) ; i++) 144 if (trailer->data[i] == ' ') 145 break; 146 147 if (i == (num_bytes - 2)) 148 return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, 149 _("Final line in revision file r%ld missing space"), 150 rev); 151 152 if (root_offset) 153 { 154 apr_int64_t val; 155 156 trailer->data[i] = '\0'; 157 SVN_ERR(svn_cstring_atoi64(&val, str)); 158 *root_offset = (apr_off_t)val; 159 } 160 161 i++; 162 str = &trailer->data[i]; 163 164 /* find the next newline */ 165 for ( ; i < num_bytes; i++) 166 if (trailer->data[i] == '\n') 167 break; 168 169 if (changes_offset) 170 { 171 apr_int64_t val; 172 173 trailer->data[i] = '\0'; 174 SVN_ERR(svn_cstring_atoi64(&val, str)); 175 *changes_offset = (apr_off_t)val; 176 } 177 178 return SVN_NO_ERROR; 179} 180 181svn_stringbuf_t * 182svn_fs_fs__unparse_revision_trailer(apr_off_t root_offset, 183 apr_off_t changes_offset, 184 apr_pool_t *result_pool) 185{ 186 return svn_stringbuf_createf(result_pool, 187 "%" APR_OFF_T_FMT " %" APR_OFF_T_FMT "\n", 188 root_offset, 189 changes_offset); 190} 191 192/* If ERR is not NULL, wrap it MESSAGE. The latter must have an %ld 193 * format parameter that will be filled with REV. */ 194static svn_error_t * 195wrap_footer_error(svn_error_t *err, 196 const char *message, 197 svn_revnum_t rev) 198{ 199 if (err) 200 return svn_error_quick_wrapf(err, message, rev); 201 202 return SVN_NO_ERROR; 203} 204 205svn_error_t * 206svn_fs_fs__parse_footer(apr_off_t *l2p_offset, 207 svn_checksum_t **l2p_checksum, 208 apr_off_t *p2l_offset, 209 svn_checksum_t **p2l_checksum, 210 svn_stringbuf_t *footer, 211 svn_revnum_t rev, 212 apr_off_t footer_offset, 213 apr_pool_t *result_pool) 214{ 215 apr_int64_t val; 216 char *last_str = footer->data; 217 218 /* Get the L2P offset. */ 219 const char *str = svn_cstring_tokenize(" ", &last_str); 220 if (str == NULL) 221 return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, 222 "Invalid r%ld footer", rev); 223 224 SVN_ERR(wrap_footer_error(svn_cstring_strtoi64(&val, str, 0, 225 footer_offset - 1, 10), 226 "Invalid L2P offset in r%ld footer", 227 rev)); 228 *l2p_offset = (apr_off_t)val; 229 230 /* Get the L2P checksum. */ 231 str = svn_cstring_tokenize(" ", &last_str); 232 if (str == NULL) 233 return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, 234 "Invalid r%ld footer", rev); 235 236 SVN_ERR(svn_checksum_parse_hex(l2p_checksum, svn_checksum_md5, str, 237 result_pool)); 238 239 /* Get the P2L offset. */ 240 str = svn_cstring_tokenize(" ", &last_str); 241 if (str == NULL) 242 return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, 243 "Invalid r%ld footer", rev); 244 245 SVN_ERR(wrap_footer_error(svn_cstring_strtoi64(&val, str, 0, 246 footer_offset - 1, 10), 247 "Invalid P2L offset in r%ld footer", 248 rev)); 249 *p2l_offset = (apr_off_t)val; 250 251 /* The P2L indes follows the L2P index */ 252 if (*p2l_offset <= *l2p_offset) 253 return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, 254 "P2L offset %s must be larger than L2P offset %s" 255 " in r%ld footer", 256 apr_psprintf(result_pool, 257 "0x%" APR_UINT64_T_HEX_FMT, 258 (apr_uint64_t)*p2l_offset), 259 apr_psprintf(result_pool, 260 "0x%" APR_UINT64_T_HEX_FMT, 261 (apr_uint64_t)*l2p_offset), 262 rev); 263 264 /* Get the P2L checksum. */ 265 str = svn_cstring_tokenize(" ", &last_str); 266 if (str == NULL) 267 return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, 268 "Invalid r%ld footer", rev); 269 270 SVN_ERR(svn_checksum_parse_hex(p2l_checksum, svn_checksum_md5, str, 271 result_pool)); 272 273 return SVN_NO_ERROR; 274} 275 276svn_stringbuf_t * 277svn_fs_fs__unparse_footer(apr_off_t l2p_offset, 278 svn_checksum_t *l2p_checksum, 279 apr_off_t p2l_offset, 280 svn_checksum_t *p2l_checksum, 281 apr_pool_t *result_pool, 282 apr_pool_t *scratch_pool) 283{ 284 return svn_stringbuf_createf(result_pool, 285 "%" APR_OFF_T_FMT " %s %" APR_OFF_T_FMT " %s", 286 l2p_offset, 287 svn_checksum_to_cstring(l2p_checksum, 288 scratch_pool), 289 p2l_offset, 290 svn_checksum_to_cstring(p2l_checksum, 291 scratch_pool)); 292} 293 294/* Read the next entry in the changes record from file FILE and store 295 the resulting change in *CHANGE_P. If there is no next record, 296 store NULL there. Perform all allocations from POOL. */ 297static svn_error_t * 298read_change(change_t **change_p, 299 svn_stream_t *stream, 300 apr_pool_t *result_pool, 301 apr_pool_t *scratch_pool) 302{ 303 svn_stringbuf_t *line; 304 svn_boolean_t eof = TRUE; 305 change_t *change; 306 char *str, *last_str, *kind_str; 307 svn_fs_path_change2_t *info; 308 309 /* Default return value. */ 310 *change_p = NULL; 311 312 SVN_ERR(svn_stream_readline(stream, &line, "\n", &eof, scratch_pool)); 313 314 /* Check for a blank line. */ 315 if (eof || (line->len == 0)) 316 return SVN_NO_ERROR; 317 318 change = apr_pcalloc(result_pool, sizeof(*change)); 319 info = &change->info; 320 last_str = line->data; 321 322 /* Get the node-id of the change. */ 323 str = svn_cstring_tokenize(" ", &last_str); 324 if (str == NULL) 325 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 326 _("Invalid changes line in rev-file")); 327 328 SVN_ERR(svn_fs_fs__id_parse(&info->node_rev_id, str, result_pool)); 329 if (info->node_rev_id == NULL) 330 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 331 _("Invalid changes line in rev-file")); 332 333 /* Get the change type. */ 334 str = svn_cstring_tokenize(" ", &last_str); 335 if (str == NULL) 336 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 337 _("Invalid changes line in rev-file")); 338 339 /* Don't bother to check the format number before looking for 340 * node-kinds: just read them if you find them. */ 341 info->node_kind = svn_node_unknown; 342 kind_str = strchr(str, '-'); 343 if (kind_str) 344 { 345 /* Cap off the end of "str" (the action). */ 346 *kind_str = '\0'; 347 kind_str++; 348 if (strcmp(kind_str, SVN_FS_FS__KIND_FILE) == 0) 349 info->node_kind = svn_node_file; 350 else if (strcmp(kind_str, SVN_FS_FS__KIND_DIR) == 0) 351 info->node_kind = svn_node_dir; 352 else 353 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 354 _("Invalid changes line in rev-file")); 355 } 356 357 if (strcmp(str, ACTION_MODIFY) == 0) 358 { 359 info->change_kind = svn_fs_path_change_modify; 360 } 361 else if (strcmp(str, ACTION_ADD) == 0) 362 { 363 info->change_kind = svn_fs_path_change_add; 364 } 365 else if (strcmp(str, ACTION_DELETE) == 0) 366 { 367 info->change_kind = svn_fs_path_change_delete; 368 } 369 else if (strcmp(str, ACTION_REPLACE) == 0) 370 { 371 info->change_kind = svn_fs_path_change_replace; 372 } 373 else if (strcmp(str, ACTION_RESET) == 0) 374 { 375 info->change_kind = svn_fs_path_change_reset; 376 } 377 else 378 { 379 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 380 _("Invalid change kind in rev file")); 381 } 382 383 /* Get the text-mod flag. */ 384 str = svn_cstring_tokenize(" ", &last_str); 385 if (str == NULL) 386 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 387 _("Invalid changes line in rev-file")); 388 389 if (strcmp(str, FLAG_TRUE) == 0) 390 { 391 info->text_mod = TRUE; 392 } 393 else if (strcmp(str, FLAG_FALSE) == 0) 394 { 395 info->text_mod = FALSE; 396 } 397 else 398 { 399 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 400 _("Invalid text-mod flag in rev-file")); 401 } 402 403 /* Get the prop-mod flag. */ 404 str = svn_cstring_tokenize(" ", &last_str); 405 if (str == NULL) 406 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 407 _("Invalid changes line in rev-file")); 408 409 if (strcmp(str, FLAG_TRUE) == 0) 410 { 411 info->prop_mod = TRUE; 412 } 413 else if (strcmp(str, FLAG_FALSE) == 0) 414 { 415 info->prop_mod = FALSE; 416 } 417 else 418 { 419 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 420 _("Invalid prop-mod flag in rev-file")); 421 } 422 423 /* Get the mergeinfo-mod flag if given. Otherwise, the next thing 424 is the path starting with a slash. Also, we must initialize the 425 flag explicitly because 0 is not valid for a svn_tristate_t. */ 426 info->mergeinfo_mod = svn_tristate_unknown; 427 if (*last_str != '/') 428 { 429 str = svn_cstring_tokenize(" ", &last_str); 430 if (str == NULL) 431 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 432 _("Invalid changes line in rev-file")); 433 434 if (strcmp(str, FLAG_TRUE) == 0) 435 { 436 info->mergeinfo_mod = svn_tristate_true; 437 } 438 else if (strcmp(str, FLAG_FALSE) == 0) 439 { 440 info->mergeinfo_mod = svn_tristate_false; 441 } 442 else 443 { 444 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 445 _("Invalid mergeinfo-mod flag in rev-file")); 446 } 447 } 448 449 /* Get the changed path. */ 450 if (!svn_fspath__is_canonical(last_str)) 451 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 452 _("Invalid path in changes line")); 453 454 change->path.len = strlen(last_str); 455 change->path.data = apr_pstrdup(result_pool, last_str); 456 457 /* Read the next line, the copyfrom line. */ 458 SVN_ERR(svn_stream_readline(stream, &line, "\n", &eof, scratch_pool)); 459 info->copyfrom_known = TRUE; 460 if (eof || line->len == 0) 461 { 462 info->copyfrom_rev = SVN_INVALID_REVNUM; 463 info->copyfrom_path = NULL; 464 } 465 else 466 { 467 last_str = line->data; 468 SVN_ERR(parse_revnum(&info->copyfrom_rev, (const char **)&last_str)); 469 470 if (!svn_fspath__is_canonical(last_str)) 471 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 472 _("Invalid copy-from path in changes line")); 473 474 info->copyfrom_path = apr_pstrdup(result_pool, last_str); 475 } 476 477 *change_p = change; 478 479 return SVN_NO_ERROR; 480} 481 482svn_error_t * 483svn_fs_fs__read_changes(apr_array_header_t **changes, 484 svn_stream_t *stream, 485 int max_count, 486 apr_pool_t *result_pool, 487 apr_pool_t *scratch_pool) 488{ 489 apr_pool_t *iterpool; 490 491 /* Pre-allocate enough room for most change lists. 492 (will be auto-expanded as necessary). 493 494 Chose the default to just below 2^N such that the doubling reallocs 495 will request roughly 2^M bytes from the OS without exceeding the 496 respective two-power by just a few bytes (leaves room array and APR 497 node overhead for large enough M). 498 */ 499 *changes = apr_array_make(result_pool, 63, sizeof(change_t *)); 500 501 iterpool = svn_pool_create(scratch_pool); 502 for (; max_count > 0; --max_count) 503 { 504 change_t *change; 505 svn_pool_clear(iterpool); 506 SVN_ERR(read_change(&change, stream, result_pool, iterpool)); 507 if (!change) 508 break; 509 510 APR_ARRAY_PUSH(*changes, change_t*) = change; 511 } 512 svn_pool_destroy(iterpool); 513 514 return SVN_NO_ERROR; 515} 516 517svn_error_t * 518svn_fs_fs__read_changes_incrementally(svn_stream_t *stream, 519 svn_fs_fs__change_receiver_t 520 change_receiver, 521 void *change_receiver_baton, 522 apr_pool_t *scratch_pool) 523{ 524 change_t *change; 525 apr_pool_t *iterpool; 526 527 iterpool = svn_pool_create(scratch_pool); 528 do 529 { 530 svn_pool_clear(iterpool); 531 532 SVN_ERR(read_change(&change, stream, iterpool, iterpool)); 533 if (change) 534 SVN_ERR(change_receiver(change_receiver_baton, change, iterpool)); 535 } 536 while (change); 537 svn_pool_destroy(iterpool); 538 539 return SVN_NO_ERROR; 540} 541 542/* Write a single change entry, path PATH, change CHANGE, to STREAM. 543 544 Only include the node kind field if INCLUDE_NODE_KIND is true. Only 545 include the mergeinfo-mod field if INCLUDE_MERGEINFO_MODS is true. 546 All temporary allocations are in SCRATCH_POOL. */ 547static svn_error_t * 548write_change_entry(svn_stream_t *stream, 549 const char *path, 550 svn_fs_path_change2_t *change, 551 svn_boolean_t include_node_kind, 552 svn_boolean_t include_mergeinfo_mods, 553 apr_pool_t *scratch_pool) 554{ 555 const char *idstr; 556 const char *change_string = NULL; 557 const char *kind_string = ""; 558 const char *mergeinfo_string = ""; 559 svn_stringbuf_t *buf; 560 apr_size_t len; 561 562 switch (change->change_kind) 563 { 564 case svn_fs_path_change_modify: 565 change_string = ACTION_MODIFY; 566 break; 567 case svn_fs_path_change_add: 568 change_string = ACTION_ADD; 569 break; 570 case svn_fs_path_change_delete: 571 change_string = ACTION_DELETE; 572 break; 573 case svn_fs_path_change_replace: 574 change_string = ACTION_REPLACE; 575 break; 576 case svn_fs_path_change_reset: 577 change_string = ACTION_RESET; 578 break; 579 default: 580 return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, 581 _("Invalid change type %d"), 582 change->change_kind); 583 } 584 585 if (change->node_rev_id) 586 idstr = svn_fs_fs__id_unparse(change->node_rev_id, scratch_pool)->data; 587 else 588 idstr = ACTION_RESET; 589 590 if (include_node_kind) 591 { 592 SVN_ERR_ASSERT(change->node_kind == svn_node_dir 593 || change->node_kind == svn_node_file); 594 kind_string = apr_psprintf(scratch_pool, "-%s", 595 change->node_kind == svn_node_dir 596 ? SVN_FS_FS__KIND_DIR 597 : SVN_FS_FS__KIND_FILE); 598 } 599 600 if (include_mergeinfo_mods && change->mergeinfo_mod != svn_tristate_unknown) 601 mergeinfo_string = apr_psprintf(scratch_pool, " %s", 602 change->mergeinfo_mod == svn_tristate_true 603 ? FLAG_TRUE 604 : FLAG_FALSE); 605 606 buf = svn_stringbuf_createf(scratch_pool, "%s %s%s %s %s%s %s\n", 607 idstr, change_string, kind_string, 608 change->text_mod ? FLAG_TRUE : FLAG_FALSE, 609 change->prop_mod ? FLAG_TRUE : FLAG_FALSE, 610 mergeinfo_string, 611 path); 612 613 if (SVN_IS_VALID_REVNUM(change->copyfrom_rev)) 614 { 615 svn_stringbuf_appendcstr(buf, apr_psprintf(scratch_pool, "%ld %s", 616 change->copyfrom_rev, 617 change->copyfrom_path)); 618 } 619 620 svn_stringbuf_appendbyte(buf, '\n'); 621 622 /* Write all change info in one write call. */ 623 len = buf->len; 624 return svn_error_trace(svn_stream_write(stream, buf->data, &len)); 625} 626 627svn_error_t * 628svn_fs_fs__write_changes(svn_stream_t *stream, 629 svn_fs_t *fs, 630 apr_hash_t *changes, 631 svn_boolean_t terminate_list, 632 apr_pool_t *scratch_pool) 633{ 634 apr_pool_t *iterpool = svn_pool_create(scratch_pool); 635 fs_fs_data_t *ffd = fs->fsap_data; 636 svn_boolean_t include_node_kinds = 637 ffd->format >= SVN_FS_FS__MIN_KIND_IN_CHANGED_FORMAT; 638 svn_boolean_t include_mergeinfo_mods = 639 ffd->format >= SVN_FS_FS__MIN_MERGEINFO_IN_CHANGED_FORMAT; 640 apr_array_header_t *sorted_changed_paths; 641 int i; 642 643 /* For the sake of the repository administrator sort the changes so 644 that the final file is deterministic and repeatable, however the 645 rest of the FSFS code doesn't require any particular order here. 646 647 Also, this sorting is only effective in writing all entries with 648 a single call as write_final_changed_path_info() does. For the 649 list being written incrementally during transaction, we actually 650 *must not* change the order of entries from different calls. 651 */ 652 sorted_changed_paths = svn_sort__hash(changes, 653 svn_sort_compare_items_lexically, 654 scratch_pool); 655 656 /* Write all items to disk in the new order. */ 657 for (i = 0; i < sorted_changed_paths->nelts; ++i) 658 { 659 svn_fs_path_change2_t *change; 660 const char *path; 661 662 svn_pool_clear(iterpool); 663 664 change = APR_ARRAY_IDX(sorted_changed_paths, i, svn_sort__item_t).value; 665 path = APR_ARRAY_IDX(sorted_changed_paths, i, svn_sort__item_t).key; 666 667 /* Write out the new entry into the final rev-file. */ 668 SVN_ERR(write_change_entry(stream, path, change, include_node_kinds, 669 include_mergeinfo_mods, iterpool)); 670 } 671 672 if (terminate_list) 673 SVN_ERR(svn_stream_puts(stream, "\n")); 674 675 svn_pool_destroy(iterpool); 676 677 return SVN_NO_ERROR; 678} 679 680/* Given a revision file FILE that has been pre-positioned at the 681 beginning of a Node-Rev header block, read in that header block and 682 store it in the apr_hash_t HEADERS. All allocations will be from 683 RESULT_POOL. */ 684static svn_error_t * 685read_header_block(apr_hash_t **headers, 686 svn_stream_t *stream, 687 apr_pool_t *result_pool) 688{ 689 *headers = svn_hash__make(result_pool); 690 691 while (1) 692 { 693 svn_stringbuf_t *header_str; 694 const char *name, *value; 695 apr_size_t i = 0; 696 apr_size_t name_len; 697 svn_boolean_t eof; 698 699 SVN_ERR(svn_stream_readline(stream, &header_str, "\n", &eof, 700 result_pool)); 701 702 if (eof || header_str->len == 0) 703 break; /* end of header block */ 704 705 while (header_str->data[i] != ':') 706 { 707 if (header_str->data[i] == '\0') 708 return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, 709 _("Found malformed header '%s' in " 710 "revision file"), 711 header_str->data); 712 i++; 713 } 714 715 /* Create a 'name' string and point to it. */ 716 header_str->data[i] = '\0'; 717 name = header_str->data; 718 name_len = i; 719 720 /* Check if we have enough data to parse. */ 721 if (i + 2 > header_str->len) 722 { 723 /* Restore the original line for the error. */ 724 header_str->data[i] = ':'; 725 return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, 726 _("Found malformed header '%s' in " 727 "revision file"), 728 header_str->data); 729 } 730 731 /* Skip over the NULL byte and the space following it. */ 732 i += 2; 733 734 value = header_str->data + i; 735 736 /* header_str is safely in our pool, so we can use bits of it as 737 key and value. */ 738 apr_hash_set(*headers, name, name_len, value); 739 } 740 741 return SVN_NO_ERROR; 742} 743 744/* ### Ouch! The implementation of this function currently modifies 745 ### the input string when tokenizing it (so the input cannot be 746 ### used after that). */ 747svn_error_t * 748svn_fs_fs__parse_representation(representation_t **rep_p, 749 svn_stringbuf_t *text, 750 apr_pool_t *result_pool, 751 apr_pool_t *scratch_pool) 752{ 753 representation_t *rep; 754 char *str; 755 apr_int64_t val; 756 char *string = text->data; 757 svn_checksum_t *checksum; 758 const char *end; 759 760 rep = apr_pcalloc(result_pool, sizeof(*rep)); 761 *rep_p = rep; 762 763 SVN_ERR(parse_revnum(&rep->revision, (const char **)&string)); 764 765 /* initialize transaction info (never stored) */ 766 svn_fs_fs__id_txn_reset(&rep->txn_id); 767 768 /* while in transactions, it is legal to simply write "-1" */ 769 str = svn_cstring_tokenize(" ", &string); 770 if (str == NULL) 771 { 772 if (rep->revision == SVN_INVALID_REVNUM) 773 return SVN_NO_ERROR; 774 775 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 776 _("Malformed text representation offset line in node-rev")); 777 } 778 779 SVN_ERR(svn_cstring_atoi64(&val, str)); 780 rep->item_index = (apr_uint64_t)val; 781 782 str = svn_cstring_tokenize(" ", &string); 783 if (str == NULL) 784 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 785 _("Malformed text representation offset line in node-rev")); 786 787 SVN_ERR(svn_cstring_atoi64(&val, str)); 788 rep->size = (svn_filesize_t)val; 789 790 str = svn_cstring_tokenize(" ", &string); 791 if (str == NULL) 792 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 793 _("Malformed text representation offset line in node-rev")); 794 795 SVN_ERR(svn_cstring_atoi64(&val, str)); 796 rep->expanded_size = (svn_filesize_t)val; 797 798 /* Read in the MD5 hash. */ 799 str = svn_cstring_tokenize(" ", &string); 800 if ((str == NULL) || (strlen(str) != (APR_MD5_DIGESTSIZE * 2))) 801 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 802 _("Malformed text representation offset line in node-rev")); 803 804 SVN_ERR(svn_checksum_parse_hex(&checksum, svn_checksum_md5, str, 805 scratch_pool)); 806 807 /* If STR is a all-zero checksum, CHECKSUM will be NULL and REP already 808 contains the correct value. */ 809 if (checksum) 810 memcpy(rep->md5_digest, checksum->digest, sizeof(rep->md5_digest)); 811 812 /* The remaining fields are only used for formats >= 4, so check that. */ 813 str = svn_cstring_tokenize(" ", &string); 814 if (str == NULL) 815 return SVN_NO_ERROR; 816 817 /* Is the SHA1 hash present? */ 818 if (str[0] == '-' && str[1] == 0) 819 { 820 checksum = NULL; 821 } 822 else 823 { 824 /* Read the SHA1 hash. */ 825 if (strlen(str) != (APR_SHA1_DIGESTSIZE * 2)) 826 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 827 _("Malformed text representation offset line in node-rev")); 828 829 SVN_ERR(svn_checksum_parse_hex(&checksum, svn_checksum_sha1, str, 830 scratch_pool)); 831 } 832 833 /* We do have a valid SHA1 but it might be all 0. 834 We cannot be sure where that came from (Alas! legacy), so let's not 835 claim we know the SHA1 in that case. */ 836 rep->has_sha1 = checksum != NULL; 837 838 /* If STR is a all-zero checksum, CHECKSUM will be NULL and REP already 839 contains the correct value. */ 840 if (checksum) 841 memcpy(rep->sha1_digest, checksum->digest, sizeof(rep->sha1_digest)); 842 843 str = svn_cstring_tokenize(" ", &string); 844 if (str == NULL) 845 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 846 _("Malformed text representation offset line in node-rev")); 847 848 /* Is the uniquifier present? */ 849 if (str[0] == '-' && str[1] == 0) 850 { 851 end = string; 852 } 853 else 854 { 855 char *substring = str; 856 857 /* Read the uniquifier. */ 858 str = svn_cstring_tokenize("/", &substring); 859 if (str == NULL) 860 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 861 _("Malformed text representation offset line in node-rev")); 862 863 SVN_ERR(svn_fs_fs__id_txn_parse(&rep->uniquifier.noderev_txn_id, str)); 864 865 str = svn_cstring_tokenize(" ", &substring); 866 if (str == NULL || *str != '_') 867 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 868 _("Malformed text representation offset line in node-rev")); 869 870 ++str; 871 rep->uniquifier.number = svn__base36toui64(&end, str); 872 } 873 874 if (*end) 875 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 876 _("Malformed text representation offset line in node-rev")); 877 878 return SVN_NO_ERROR; 879} 880 881/* Wrap svn_fs_fs__parse_representation(), extracting its TXN_ID from our 882 NODEREV_ID, and adding an error message. */ 883static svn_error_t * 884read_rep_offsets(representation_t **rep_p, 885 char *string, 886 const svn_fs_id_t *noderev_id, 887 apr_pool_t *result_pool, 888 apr_pool_t *scratch_pool) 889{ 890 svn_error_t *err 891 = svn_fs_fs__parse_representation(rep_p, 892 svn_stringbuf_create_wrap(string, 893 scratch_pool), 894 result_pool, 895 scratch_pool); 896 if (err) 897 { 898 const svn_string_t *id_unparsed; 899 const char *where; 900 901 id_unparsed = svn_fs_fs__id_unparse(noderev_id, scratch_pool); 902 where = apr_psprintf(scratch_pool, 903 _("While reading representation offsets " 904 "for node-revision '%s':"), 905 noderev_id ? id_unparsed->data : "(null)"); 906 907 return svn_error_quick_wrap(err, where); 908 } 909 910 if ((*rep_p)->revision == SVN_INVALID_REVNUM) 911 if (noderev_id) 912 (*rep_p)->txn_id = *svn_fs_fs__id_txn_id(noderev_id); 913 914 return SVN_NO_ERROR; 915} 916 917svn_error_t * 918svn_fs_fs__read_noderev(node_revision_t **noderev_p, 919 svn_stream_t *stream, 920 apr_pool_t *result_pool, 921 apr_pool_t *scratch_pool) 922{ 923 apr_hash_t *headers; 924 node_revision_t *noderev; 925 char *value; 926 const char *noderev_id; 927 928 SVN_ERR(read_header_block(&headers, stream, scratch_pool)); 929 930 noderev = apr_pcalloc(result_pool, sizeof(*noderev)); 931 932 /* Read the node-rev id. */ 933 value = svn_hash_gets(headers, HEADER_ID); 934 if (value == NULL) 935 /* ### More information: filename/offset coordinates */ 936 return svn_error_create(SVN_ERR_FS_CORRUPT, NULL, 937 _("Missing id field in node-rev")); 938 939 SVN_ERR(svn_stream_close(stream)); 940 941 SVN_ERR(svn_fs_fs__id_parse(&noderev->id, value, result_pool)); 942 noderev_id = value; /* for error messages later */ 943 944 /* Read the type. */ 945 value = svn_hash_gets(headers, HEADER_TYPE); 946 947 if ((value == NULL) || 948 ( strcmp(value, SVN_FS_FS__KIND_FILE) 949 && strcmp(value, SVN_FS_FS__KIND_DIR))) 950 /* ### s/kind/type/ */ 951 return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, 952 _("Missing kind field in node-rev '%s'"), 953 noderev_id); 954 955 noderev->kind = (strcmp(value, SVN_FS_FS__KIND_FILE) == 0) 956 ? svn_node_file 957 : svn_node_dir; 958 959 /* Read the 'count' field. */ 960 value = svn_hash_gets(headers, HEADER_COUNT); 961 if (value) 962 SVN_ERR(svn_cstring_atoi(&noderev->predecessor_count, value)); 963 else 964 noderev->predecessor_count = 0; 965 966 /* Get the properties location. */ 967 value = svn_hash_gets(headers, HEADER_PROPS); 968 if (value) 969 { 970 SVN_ERR(read_rep_offsets(&noderev->prop_rep, value, 971 noderev->id, result_pool, scratch_pool)); 972 } 973 974 /* Get the data location. */ 975 value = svn_hash_gets(headers, HEADER_TEXT); 976 if (value) 977 { 978 SVN_ERR(read_rep_offsets(&noderev->data_rep, value, 979 noderev->id, result_pool, scratch_pool)); 980 } 981 982 /* Get the created path. */ 983 value = svn_hash_gets(headers, HEADER_CPATH); 984 if (value == NULL) 985 { 986 return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, 987 _("Missing cpath field in node-rev '%s'"), 988 noderev_id); 989 } 990 else 991 { 992 if (!svn_fspath__is_canonical(value)) 993 return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, 994 _("Non-canonical cpath field in node-rev '%s'"), 995 noderev_id); 996 997 noderev->created_path = apr_pstrdup(result_pool, value); 998 } 999 1000 /* Get the predecessor ID. */ 1001 value = svn_hash_gets(headers, HEADER_PRED); 1002 if (value) 1003 SVN_ERR(svn_fs_fs__id_parse(&noderev->predecessor_id, value, 1004 result_pool)); 1005 1006 /* Get the copyroot. */ 1007 value = svn_hash_gets(headers, HEADER_COPYROOT); 1008 if (value == NULL) 1009 { 1010 noderev->copyroot_path = apr_pstrdup(result_pool, noderev->created_path); 1011 noderev->copyroot_rev = svn_fs_fs__id_rev(noderev->id); 1012 } 1013 else 1014 { 1015 SVN_ERR(parse_revnum(&noderev->copyroot_rev, (const char **)&value)); 1016 1017 if (!svn_fspath__is_canonical(value)) 1018 return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, 1019 _("Malformed copyroot line in node-rev '%s'"), 1020 noderev_id); 1021 noderev->copyroot_path = apr_pstrdup(result_pool, value); 1022 } 1023 1024 /* Get the copyfrom. */ 1025 value = svn_hash_gets(headers, HEADER_COPYFROM); 1026 if (value == NULL) 1027 { 1028 noderev->copyfrom_path = NULL; 1029 noderev->copyfrom_rev = SVN_INVALID_REVNUM; 1030 } 1031 else 1032 { 1033 SVN_ERR(parse_revnum(&noderev->copyfrom_rev, (const char **)&value)); 1034 1035 if (*value == 0) 1036 return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, 1037 _("Malformed copyfrom line in node-rev '%s'"), 1038 noderev_id); 1039 noderev->copyfrom_path = apr_pstrdup(result_pool, value); 1040 } 1041 1042 /* Get whether this is a fresh txn root. */ 1043 value = svn_hash_gets(headers, HEADER_FRESHTXNRT); 1044 noderev->is_fresh_txn_root = (value != NULL); 1045 1046 /* Get the mergeinfo count. */ 1047 value = svn_hash_gets(headers, HEADER_MINFO_CNT); 1048 if (value) 1049 SVN_ERR(svn_cstring_atoi64(&noderev->mergeinfo_count, value)); 1050 else 1051 noderev->mergeinfo_count = 0; 1052 1053 /* Get whether *this* node has mergeinfo. */ 1054 value = svn_hash_gets(headers, HEADER_MINFO_HERE); 1055 noderev->has_mergeinfo = (value != NULL); 1056 1057 *noderev_p = noderev; 1058 1059 return SVN_NO_ERROR; 1060} 1061 1062/* Return a textual representation of the DIGEST of given KIND. 1063 * Allocate the result in RESULT_POOL. 1064 */ 1065static const char * 1066format_digest(const unsigned char *digest, 1067 svn_checksum_kind_t kind, 1068 apr_pool_t *result_pool) 1069{ 1070 svn_checksum_t checksum; 1071 checksum.digest = digest; 1072 checksum.kind = kind; 1073 1074 return svn_checksum_to_cstring_display(&checksum, result_pool); 1075} 1076 1077/* Return a textual representation of the uniquifier represented 1078 * by NODEREV_TXN_ID and NUMBER. Use POOL for the allocations. 1079 */ 1080static const char * 1081format_uniquifier(const svn_fs_fs__id_part_t *noderev_txn_id, 1082 apr_uint64_t number, 1083 apr_pool_t *pool) 1084{ 1085 char buf[SVN_INT64_BUFFER_SIZE]; 1086 const char *txn_id_str; 1087 1088 txn_id_str = svn_fs_fs__id_txn_unparse(noderev_txn_id, pool); 1089 svn__ui64tobase36(buf, number); 1090 1091 return apr_psprintf(pool, "%s/_%s", txn_id_str, buf); 1092} 1093 1094svn_stringbuf_t * 1095svn_fs_fs__unparse_representation(representation_t *rep, 1096 int format, 1097 svn_boolean_t mutable_rep_truncated, 1098 apr_pool_t *result_pool, 1099 apr_pool_t *scratch_pool) 1100{ 1101 svn_stringbuf_t *str; 1102 const char *sha1_str; 1103 const char *uniquifier_str; 1104 1105 if (svn_fs_fs__id_txn_used(&rep->txn_id) && mutable_rep_truncated) 1106 return svn_stringbuf_ncreate("-1", 2, result_pool); 1107 1108 /* Format of the string: 1109 <rev> <item_index> <size> <expanded-size> <md5> [<sha1>] [<uniquifier>] 1110 */ 1111 str = svn_stringbuf_createf( 1112 result_pool, 1113 "%ld" 1114 " %" APR_UINT64_T_FMT 1115 " %" SVN_FILESIZE_T_FMT 1116 " %" SVN_FILESIZE_T_FMT 1117 " %s", 1118 rep->revision, 1119 rep->item_index, 1120 rep->size, 1121 rep->expanded_size, 1122 format_digest(rep->md5_digest, svn_checksum_md5, scratch_pool)); 1123 1124 /* Compatibility: these formats don't understand <sha1> and <uniquifier>. */ 1125 if (format < SVN_FS_FS__MIN_REP_SHARING_FORMAT) 1126 return str; 1127 1128 if (format < SVN_FS_FS__MIN_REP_STRING_OPTIONAL_VALUES_FORMAT) 1129 { 1130 /* Compatibility: these formats can only have <sha1> and <uniquifier> 1131 present simultaneously, or don't have them at all. */ 1132 if (rep->has_sha1) 1133 { 1134 sha1_str = format_digest(rep->sha1_digest, svn_checksum_sha1, 1135 scratch_pool); 1136 uniquifier_str = format_uniquifier(&rep->uniquifier.noderev_txn_id, 1137 rep->uniquifier.number, 1138 scratch_pool); 1139 svn_stringbuf_appendbyte(str, ' '); 1140 svn_stringbuf_appendcstr(str, sha1_str); 1141 svn_stringbuf_appendbyte(str, ' '); 1142 svn_stringbuf_appendcstr(str, uniquifier_str); 1143 } 1144 return str; 1145 } 1146 1147 /* The most recent formats support optional <sha1> and <uniquifier> values. */ 1148 if (rep->has_sha1) 1149 { 1150 sha1_str = format_digest(rep->sha1_digest, svn_checksum_sha1, 1151 scratch_pool); 1152 } 1153 else 1154 sha1_str = "-"; 1155 1156 if (rep->uniquifier.number == 0 && 1157 rep->uniquifier.noderev_txn_id.number == 0 && 1158 rep->uniquifier.noderev_txn_id.revision == 0) 1159 { 1160 uniquifier_str = "-"; 1161 } 1162 else 1163 { 1164 uniquifier_str = format_uniquifier(&rep->uniquifier.noderev_txn_id, 1165 rep->uniquifier.number, 1166 scratch_pool); 1167 } 1168 1169 svn_stringbuf_appendbyte(str, ' '); 1170 svn_stringbuf_appendcstr(str, sha1_str); 1171 svn_stringbuf_appendbyte(str, ' '); 1172 svn_stringbuf_appendcstr(str, uniquifier_str); 1173 1174 return str; 1175} 1176 1177 1178svn_error_t * 1179svn_fs_fs__write_noderev(svn_stream_t *outfile, 1180 node_revision_t *noderev, 1181 int format, 1182 svn_boolean_t include_mergeinfo, 1183 apr_pool_t *scratch_pool) 1184{ 1185 SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_ID ": %s\n", 1186 svn_fs_fs__id_unparse(noderev->id, 1187 scratch_pool)->data)); 1188 1189 SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_TYPE ": %s\n", 1190 (noderev->kind == svn_node_file) ? 1191 SVN_FS_FS__KIND_FILE : SVN_FS_FS__KIND_DIR)); 1192 1193 if (noderev->predecessor_id) 1194 SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_PRED ": %s\n", 1195 svn_fs_fs__id_unparse(noderev->predecessor_id, 1196 scratch_pool)->data)); 1197 1198 SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_COUNT ": %d\n", 1199 noderev->predecessor_count)); 1200 1201 if (noderev->data_rep) 1202 SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_TEXT ": %s\n", 1203 svn_fs_fs__unparse_representation 1204 (noderev->data_rep, 1205 format, 1206 noderev->kind == svn_node_dir, 1207 scratch_pool, scratch_pool)->data)); 1208 1209 if (noderev->prop_rep) 1210 SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_PROPS ": %s\n", 1211 svn_fs_fs__unparse_representation 1212 (noderev->prop_rep, format, 1213 TRUE, scratch_pool, scratch_pool)->data)); 1214 1215 SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_CPATH ": %s\n", 1216 noderev->created_path)); 1217 1218 if (noderev->copyfrom_path) 1219 SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_COPYFROM ": %ld" 1220 " %s\n", 1221 noderev->copyfrom_rev, 1222 noderev->copyfrom_path)); 1223 1224 if ((noderev->copyroot_rev != svn_fs_fs__id_rev(noderev->id)) || 1225 (strcmp(noderev->copyroot_path, noderev->created_path) != 0)) 1226 SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_COPYROOT ": %ld" 1227 " %s\n", 1228 noderev->copyroot_rev, 1229 noderev->copyroot_path)); 1230 1231 if (noderev->is_fresh_txn_root) 1232 SVN_ERR(svn_stream_puts(outfile, HEADER_FRESHTXNRT ": y\n")); 1233 1234 if (include_mergeinfo) 1235 { 1236 if (noderev->mergeinfo_count > 0) 1237 SVN_ERR(svn_stream_printf(outfile, scratch_pool, HEADER_MINFO_CNT 1238 ": %" APR_INT64_T_FMT "\n", 1239 noderev->mergeinfo_count)); 1240 1241 if (noderev->has_mergeinfo) 1242 SVN_ERR(svn_stream_puts(outfile, HEADER_MINFO_HERE ": y\n")); 1243 } 1244 1245 return svn_stream_puts(outfile, "\n"); 1246} 1247 1248svn_error_t * 1249svn_fs_fs__read_rep_header(svn_fs_fs__rep_header_t **header, 1250 svn_stream_t *stream, 1251 apr_pool_t *result_pool, 1252 apr_pool_t *scratch_pool) 1253{ 1254 svn_stringbuf_t *buffer; 1255 char *str, *last_str; 1256 apr_int64_t val; 1257 svn_boolean_t eol = FALSE; 1258 1259 SVN_ERR(svn_stream_readline(stream, &buffer, "\n", &eol, scratch_pool)); 1260 1261 *header = apr_pcalloc(result_pool, sizeof(**header)); 1262 (*header)->header_size = buffer->len + 1; 1263 if (strcmp(buffer->data, REP_PLAIN) == 0) 1264 { 1265 (*header)->type = svn_fs_fs__rep_plain; 1266 return SVN_NO_ERROR; 1267 } 1268 1269 if (strcmp(buffer->data, REP_DELTA) == 0) 1270 { 1271 /* This is a delta against the empty stream. */ 1272 (*header)->type = svn_fs_fs__rep_self_delta; 1273 return SVN_NO_ERROR; 1274 } 1275 1276 (*header)->type = svn_fs_fs__rep_delta; 1277 1278 /* We have hopefully a DELTA vs. a non-empty base revision. */ 1279 last_str = buffer->data; 1280 str = svn_cstring_tokenize(" ", &last_str); 1281 if (! str || (strcmp(str, REP_DELTA) != 0)) 1282 goto error; 1283 1284 SVN_ERR(parse_revnum(&(*header)->base_revision, (const char **)&last_str)); 1285 1286 str = svn_cstring_tokenize(" ", &last_str); 1287 if (! str) 1288 goto error; 1289 SVN_ERR(svn_cstring_atoi64(&val, str)); 1290 (*header)->base_item_index = (apr_off_t)val; 1291 1292 str = svn_cstring_tokenize(" ", &last_str); 1293 if (! str) 1294 goto error; 1295 SVN_ERR(svn_cstring_atoi64(&val, str)); 1296 (*header)->base_length = (svn_filesize_t)val; 1297 1298 return SVN_NO_ERROR; 1299 1300 error: 1301 return svn_error_createf(SVN_ERR_FS_CORRUPT, NULL, 1302 _("Malformed representation header")); 1303} 1304 1305svn_error_t * 1306svn_fs_fs__write_rep_header(svn_fs_fs__rep_header_t *header, 1307 svn_stream_t *stream, 1308 apr_pool_t *scratch_pool) 1309{ 1310 const char *text; 1311 1312 switch (header->type) 1313 { 1314 case svn_fs_fs__rep_plain: 1315 text = REP_PLAIN "\n"; 1316 break; 1317 1318 case svn_fs_fs__rep_self_delta: 1319 text = REP_DELTA "\n"; 1320 break; 1321 1322 default: 1323 text = apr_psprintf(scratch_pool, REP_DELTA " %ld %" APR_OFF_T_FMT 1324 " %" SVN_FILESIZE_T_FMT "\n", 1325 header->base_revision, header->base_item_index, 1326 header->base_length); 1327 } 1328 1329 return svn_error_trace(svn_stream_puts(stream, text)); 1330} 1331