1 /* 2 * Filesystem client functions. 3 * 4 * Copyright (C) 2018-2024 Paul Boddie <paul@boddie.org.uk> 5 * 6 * This program is free software; you can redistribute it and/or 7 * modify it under the terms of the GNU General Public License as 8 * published by the Free Software Foundation; either version 2 of 9 * the License, or (at your option) any later version. 10 * 11 * This program is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with this program; if not, write to the Free Software 18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, 19 * Boston, MA 02110-1301, USA 20 */ 21 22 #include <l4/re/env.h> 23 24 #include <stdio.h> 25 #include <stdlib.h> 26 #include <string.h> 27 28 #include <mem/memory_utils.h> 29 #include <systypes/env.h> 30 #include <systypes/fcntl.h> 31 32 #include "client.h" 33 34 35 36 /* Default size of pipe regions. */ 37 38 static const offset_t DEFAULT_PIPE_SIZE = 4096; 39 40 /* Size of the core member region of a directory entry structure. */ 41 42 static const offset_t DIRENT_CORE_SIZE = (sizeof(struct dirent) - sizeof(((struct dirent *) 0)->d_name)); 43 44 45 46 /* Merging of region flags from protection and access flags. */ 47 48 static rm_flags_t _combine_region_flags(rm_flags_t region_flags, flags_t flags) 49 { 50 return region_flags & (file_region_flags(flags) | L4RE_RM_F_X); 51 } 52 53 /* Conversion of protection and access flags to region flags. */ 54 55 rm_flags_t client_region_flags(prot_t prot, flags_t flags) 56 { 57 rm_flags_t rm_flags = 0; 58 59 if (prot & PROT_READ) 60 rm_flags |= L4RE_RM_F_R; 61 if (prot & PROT_WRITE) 62 rm_flags |= L4RE_RM_F_W; 63 if (prot & PROT_EXEC) 64 rm_flags |= L4RE_RM_F_X; 65 66 return _combine_region_flags(rm_flags, flags); 67 } 68 69 70 71 /* Access the given position and synchronise state with the file object. Pipe 72 objects may return busy conditions indicating that the desired access cannot 73 yet be fulfilled. */ 74 75 static long _access(file_t *file, offset_t position) 76 { 77 long err; 78 79 if (file->object_flags & OBJECT_SUPPORTS_MMAP) 80 { 81 offset_t adjusted_position = position; 82 83 /* Where the position is outside the current region, re-map. */ 84 85 if ((position < file->start_pos) || (position >= file->end_pos)) 86 { 87 offset_t length = file_span(file); 88 89 if (!length) 90 length = PAGE_SIZE; 91 92 /* Avoid growth of the mapped region when the end of the region is 93 calculated to be position + span, which is then rounded up, whereas the 94 start of the region is rounded down. */ 95 96 adjusted_position = trunc(position, PAGE_SIZE); 97 98 if (file_mmap(file, adjusted_position, length, 0, 0, 99 file_region_flags(file->flags))) 100 return -L4_EIO; 101 } 102 103 /* Otherwise, flush any written data in the current region and update the 104 file size details. */ 105 106 else 107 { 108 err = client_flush(file); 109 if (err) 110 return err; 111 } 112 113 /* Update the current data offset. */ 114 115 file->data_current = position - file->start_pos; 116 117 return L4_EOK; 118 } 119 else 120 { 121 bool initial = (file->memory == NULL); 122 123 /* Handle the initial condition with no current region. */ 124 125 if (initial) 126 { 127 err = client_current_region(file); 128 if (err) 129 return err; 130 } 131 132 /* Strict conditions for region navigation in pipes. */ 133 134 if ((position < file->start_pos) || (position > file->end_pos)) 135 { 136 return -L4_EIO; 137 } 138 139 /* The next region is only available at the end of the mapped memory. */ 140 141 else if (position == file->end_pos) 142 { 143 return client_next_region(file); 144 } 145 146 /* Within the current pipe region, synchronise with the pipe object. */ 147 148 else 149 { 150 if (initial) 151 return L4_EOK; 152 else 153 return client_current_region(file); 154 } 155 } 156 } 157 158 159 160 /* Return whether an operation on file should block for more content or more 161 space. A file must be configured for blocking, not be closed, and must either 162 be lacking content (if reading) or space (if writing). */ 163 164 static int _operation_blocking(file_t *file, int reading) 165 { 166 return (file->can_block && !(file->notifiable.notifications & NOTIFY_PEER_CLOSED) && ( 167 (reading && !file_data_available(file)) || 168 (!reading && !file_data_space(file)))); 169 } 170 171 172 173 /* Return whether an access could occur, blocking if necessary. */ 174 175 static int _access_blocking(file_t *file, offset_t position, int reading) 176 { 177 long err; 178 179 /* Attempt to access the position, handling an error condition or a blocking 180 condition. */ 181 182 while ((err = _access(file, position)) || _operation_blocking(file, reading)) 183 { 184 position = file->data_current; 185 186 /* Exit if blocking is not configured or suitable. */ 187 188 if ((err && (err != -L4_EBUSY)) || !file->can_block) 189 return 0; 190 191 /* Handle an inability to access by blocking, exiting if waiting failed. */ 192 193 if (client_wait_file(file, client_notifier_task())) 194 return 0; 195 } 196 197 return 1; 198 } 199 200 201 202 /* Ensure that memory is mapped for accessing the given file, using the 203 indicated count as a region size hint. */ 204 205 static void *_map_memory(file_t *file, offset_t count) 206 { 207 if (file->memory == NULL) 208 { 209 if (file->object_flags & OBJECT_SUPPORTS_MMAP) 210 return client_mmap(file, client_tell(file), count, 0, 0, 211 file_region_flags(file->flags)); 212 else if (pipe_current(file)) 213 return NULL; 214 } 215 216 return file->memory; 217 } 218 219 220 221 /* Open a file opening object. */ 222 223 l4_cap_idx_t client_open_for_user(user_t user) 224 { 225 l4_cap_idx_t server = l4re_env_get_cap(ENV_FILESYSTEM_SERVER_NAME); 226 227 return client_open_for_user_using(user, server); 228 } 229 230 /* Open a file opening object via a named capability. */ 231 232 l4_cap_idx_t client_open_for_user_using(user_t user, l4_cap_idx_t server) 233 { 234 if (l4_is_invalid_cap(server)) 235 return L4_INVALID_CAP; 236 237 l4_cap_idx_t opener; 238 long err = file_open_for_user(user, server, &opener); 239 240 if (err) 241 return L4_INVALID_CAP; 242 243 return opener; 244 } 245 246 247 248 /* Close a filesystem object. */ 249 250 void client_close(file_t *file) 251 { 252 if (file == NULL) 253 return; 254 255 if (file->metadata != NULL) 256 client_close(file->metadata); 257 258 file_flush(file); 259 file_close(file); 260 free(file); 261 } 262 263 264 265 /* Obtain a stream from the environment. */ 266 267 file_t *client_get_stream(const char *name, flags_t flags) 268 { 269 l4_cap_idx_t ref = l4re_env_get_cap(name); 270 file_t *stream; 271 272 if (l4_is_invalid_cap(ref)) 273 return NULL; 274 275 stream = (file_t *) malloc(sizeof(file_t)); 276 277 file_init(stream); 278 stream->flags = flags; 279 stream->ref = ref; 280 281 /* Restore the state of the stream. */ 282 283 client_sync_stream(stream); 284 285 /* Enforce blocking if necessary. 286 NOTE: Ignoring any event subscription error. */ 287 288 if (!(flags & O_NONBLOCK)) 289 { 290 notify_flags_t nflags = 0; 291 292 if ((flags & O_WRONLY) || (flags & O_RDWR)) 293 nflags |= NOTIFY_SPACE_AVAILABLE; 294 295 if ((flags & O_RDONLY) || (flags & O_RDWR)) 296 nflags |= NOTIFY_CONTENT_AVAILABLE; 297 298 client_set_blocking(stream, nflags | NOTIFY_PEER_CLOSED); 299 } 300 301 return stream; 302 } 303 304 /* Initialise the stream data position to the end of any existing data for an 305 output stream. For an input stream, initialise the position to the end of 306 consumed data, preserving any unconsumed data. */ 307 308 long client_sync_stream(file_t *file) 309 { 310 if (!client_opened(file)) 311 return -L4_EINVAL; 312 313 return file_refresh(file); 314 } 315 316 317 318 /* Open a filesystem object. */ 319 320 file_t *client_open(const char *name, flags_t flags) 321 { 322 l4_cap_idx_t server = l4re_env_get_cap(ENV_FILESYSTEM_SERVER_NAME); 323 324 return client_open_using(name, flags, server); 325 } 326 327 /* Open a filesystem object via a named capability. */ 328 329 file_t *client_open_using(const char *name, flags_t flags, l4_cap_idx_t server) 330 { 331 if (l4_is_invalid_cap(server)) 332 return NULL; 333 334 file_t *file = (file_t *) malloc(sizeof(file_t)); 335 336 if (file == NULL) 337 return NULL; 338 339 /* Return any allocated structure even if an error occurs. */ 340 341 file->error = file_open(file, name, flags, server); 342 return file; 343 } 344 345 346 347 /* Open a directory listing stream via the given named directory. */ 348 349 file_t *client_opendir(const char *name) 350 { 351 l4_cap_idx_t server = l4re_env_get_cap(ENV_FILESYSTEM_SERVER_NAME); 352 353 return client_opendir_using(name, server); 354 } 355 356 /* Open a directory listing stream via the given named directory and a named 357 capability, returning the directory object with a metadata file object 358 reference to the directory listing stream. */ 359 360 file_t *client_opendir_using(const char *name, l4_cap_idx_t server) 361 { 362 file_t *file = client_open_using(name, O_DIRECTORY, server); 363 364 /* Return the directory structure itself for error handling. */ 365 366 if (!client_opened(file)) 367 return file; 368 369 file->metadata = client_opendir_reader(file); 370 371 /* Return the directory even if an error occurs. */ 372 373 return file; 374 } 375 376 377 378 /* Open a directory listing stream via the given directory. */ 379 380 file_t *client_opendir_reader(file_t *file) 381 { 382 file_t *reader = (file_t *) malloc(sizeof(file_t)); 383 384 if (reader == NULL) 385 return NULL; 386 387 /* Return any allocated structure even if an error occurs. */ 388 389 reader->error = directory_opendir(file, reader); 390 391 /* Set blocking read mode to be able to conveniently read directory entries 392 from the stream. If this fails, the error is set on the structure, but the 393 stream will be open. */ 394 395 if (!reader->error) 396 reader->error = client_set_blocking(reader, NOTIFY_CONTENT_AVAILABLE | NOTIFY_PEER_CLOSED); 397 398 return reader; 399 } 400 401 402 403 /* Open another instance of an opened filesystem object. */ 404 405 file_t *client_reopen(file_t *file, flags_t flags) 406 { 407 if (file == NULL) 408 return NULL; 409 410 file_t *new_file = (file_t *) malloc(sizeof(file_t)); 411 412 if (new_file == NULL) 413 return NULL; 414 415 /* Return any allocated structure even if an error occurs. */ 416 417 new_file->error = file_reopen(file, new_file, flags); 418 return new_file; 419 } 420 421 422 423 /* Open a pipe object, returning any error condition. */ 424 425 long client_pipe(file_t **reader, file_t **writer, flags_t flags) 426 { 427 l4_cap_idx_t server = l4re_env_get_cap(ENV_PIPE_SERVER_NAME); 428 429 return client_pipe_using(reader, writer, flags, server); 430 } 431 432 long client_pipe_using(file_t **reader, file_t **writer, flags_t flags, l4_cap_idx_t server) 433 { 434 *reader = NULL; 435 *writer = NULL; 436 437 if (l4_is_invalid_cap(server)) 438 return -L4_EINVAL; 439 440 *reader = (file_t *) malloc(sizeof(file_t)); 441 442 if (*reader == NULL) 443 return -L4_ENOMEM; 444 445 *writer = (file_t *) malloc(sizeof(file_t)); 446 447 if (*writer == NULL) 448 { 449 free(*reader); 450 return -L4_ENOMEM; 451 } 452 453 long err = pipe_open(DEFAULT_PIPE_SIZE, *reader, *writer, server); 454 455 /* Set blocking if successful and non-blocking is not indicated. */ 456 457 if (!err && !(flags & O_NONBLOCK)) 458 { 459 err = client_set_blocking(*reader, NOTIFY_CONTENT_AVAILABLE | NOTIFY_PEER_CLOSED); 460 if (!err) 461 err = client_set_blocking(*writer, NOTIFY_SPACE_AVAILABLE | NOTIFY_PEER_CLOSED); 462 } 463 464 if (err) 465 { 466 free(*reader); 467 free(*writer); 468 } 469 470 return err; 471 } 472 473 474 475 /* Determine whether a file has been successfully opened. */ 476 477 int client_opened(file_t *file) 478 { 479 return (file != NULL) && !file->error; 480 } 481 482 483 484 /* Make a directory in the filesystem. */ 485 486 long client_mkdir(const char *path, mode_t mode) 487 { 488 l4_cap_idx_t server = l4re_env_get_cap(ENV_FILESYSTEM_SERVER_NAME); 489 490 return client_mkdir_using(path, mode, server); 491 } 492 493 /* Make a directory in the filesystem via a named capability. */ 494 495 long client_mkdir_using(const char *path, mode_t mode, l4_cap_idx_t server) 496 { 497 return file_mkdir(path, mode, server); 498 } 499 500 501 502 /* Remove a file from the filesystem. */ 503 504 long client_remove(const char *path) 505 { 506 l4_cap_idx_t server = l4re_env_get_cap(ENV_FILESYSTEM_SERVER_NAME); 507 508 return client_remove_using(path, server); 509 } 510 511 /* Remove a file from the filesystem via a named capability. */ 512 513 long client_remove_using(const char *path, l4_cap_idx_t server) 514 { 515 return file_remove(path, server); 516 } 517 518 519 520 /* Rename a file in the filesystem. */ 521 522 long client_rename(const char *source, const char *target) 523 { 524 l4_cap_idx_t server = l4re_env_get_cap(ENV_FILESYSTEM_SERVER_NAME); 525 526 return client_rename_using(source, target, server); 527 } 528 529 /* Rename a file in the filesystem via a named capability. */ 530 531 long client_rename_using(const char *source, const char *target, l4_cap_idx_t server) 532 { 533 return file_rename(source, target, server); 534 } 535 536 537 538 /* Obtain filesystem object statistics. */ 539 540 long client_stat(const char *path, struct stat *st) 541 { 542 l4_cap_idx_t server = l4re_env_get_cap(ENV_FILESYSTEM_SERVER_NAME); 543 544 return client_stat_using(path, st, server); 545 } 546 547 /* Obtain object statistics from the filesystem via a named capability. */ 548 549 long client_stat_using(const char *path, struct stat *st, l4_cap_idx_t server) 550 { 551 return file_stat(path, st, server); 552 } 553 554 555 556 /* Obtain the current region of a pipe, updating the extent of populated data 557 and only changing the current position if no populated data exists. */ 558 559 long client_current_region(file_t *file) 560 { 561 if (!client_opened(file)) 562 return -L4_EINVAL; 563 564 return pipe_current(file); 565 } 566 567 568 569 /* Flush data explicitly to the filesystem object. */ 570 571 long client_flush(file_t *file) 572 { 573 if (!client_opened(file)) 574 return -L4_EINVAL; 575 576 /* Flush and retain most buffer settings. */ 577 578 return file_flush(file); 579 } 580 581 582 583 /* Map a memory region to a file. */ 584 585 void *client_mmap(file_t *file, offset_t position, offset_t length, 586 offset_t start_visible, offset_t end_visible, 587 rm_flags_t region_flags) 588 { 589 if (!client_opened(file) || file_mmap(file, position, length, start_visible, 590 end_visible, region_flags)) 591 return NULL; 592 593 return file->memory; 594 } 595 596 597 598 /* Obtain the next region of a pipe. */ 599 600 long client_next_region(file_t *file) 601 { 602 if (!client_opened(file)) 603 return -L4_EINVAL; 604 605 return pipe_next(file); 606 } 607 608 609 610 /* Close a notifier object. */ 611 612 void client_notifier_close(notifier_t *notifier) 613 { 614 notify_close(notifier); 615 } 616 617 /* Obtain a local notifier object. */ 618 619 notifier_t *client_notifier_local() 620 { 621 return notify_get_local(); 622 } 623 624 /* Obtain a task-wide notifier object. */ 625 626 notifier_t *client_notifier_task() 627 { 628 return notify_get_task(); 629 } 630 631 632 633 /* Read a directory entry. This must be freed by the caller after use. */ 634 635 struct dirent *client_readdir(file_t *file) 636 { 637 if ((file == NULL) || (file->metadata == NULL)) 638 return NULL; 639 640 char buffer[DIRENT_CORE_SIZE]; 641 offset_t nread = client_read(file->metadata, buffer, DIRENT_CORE_SIZE); 642 643 /* Stop if no new structure can be successfully read. */ 644 645 if (nread != DIRENT_CORE_SIZE) 646 return NULL; 647 648 struct dirent *dirent = (struct dirent *) buffer; 649 offset_t remaining = dirent->d_reclen - DIRENT_CORE_SIZE; 650 651 /* Allocate a buffer for the complete structure. */ 652 653 char *entry = (char *) calloc(DIRENT_CORE_SIZE + remaining, sizeof(char)); 654 655 if (entry == NULL) 656 return NULL; 657 658 /* Copy the start of the entry into a new buffer. */ 659 660 memcpy(entry, buffer, DIRENT_CORE_SIZE); 661 662 /* Append to the entry buffer. */ 663 664 char *current = entry + DIRENT_CORE_SIZE; 665 666 nread = client_read(file->metadata, current, remaining); 667 668 /* Stop if no complete structure can be successfully read. */ 669 670 if (nread != remaining) 671 { 672 free(entry); 673 return NULL; 674 } 675 676 return (struct dirent *) entry; 677 } 678 679 /* Rewind directory reading. */ 680 681 void client_rewinddir(file_t *file) 682 { 683 if ((file == NULL) || (file->metadata == NULL)) 684 return; 685 686 client_close(file->metadata); 687 688 file->metadata = client_opendir_reader(file); 689 } 690 691 692 693 /* Read from the filesystem object into the buffer provided. */ 694 695 offset_t client_read(file_t *file, void *buf, offset_t count) 696 { 697 if (!client_opened(file)) 698 return 0; 699 700 /* Map memory if none has been mapped so far. */ 701 702 if (_map_memory(file, count) == NULL) 703 return 0; 704 705 /* Amount available in the descriptor buffer already. */ 706 707 offset_t available = file_data_available(file); 708 offset_t to_transfer, total = 0; 709 710 while (count > 0) 711 { 712 /* If there is no data, try and obtain more data. */ 713 714 if (!available) 715 { 716 /* Flush any unwritten data, preparing to read from the file position at 717 the end of the data, and returning if no new data is available. */ 718 719 if (!_access_blocking(file, file_data_end_position(file), 1)) 720 break; 721 722 available = file_data_available(file); 723 724 if (!available) 725 break; 726 } 727 728 /* Transfer data into the supplied buffer. */ 729 730 to_transfer = available <= count ? available : count; 731 732 file_data_read(file, (char *) buf, to_transfer); 733 734 /* Update counters. */ 735 736 available -= to_transfer; 737 738 count -= to_transfer; 739 total += to_transfer; 740 741 buf = ((char *) buf + to_transfer); 742 } 743 744 return total; 745 } 746 747 748 749 /* Ensure that the buffer can provide the needed data. */ 750 751 offset_t client_seek(file_t *file, offset_t offset, int whence) 752 { 753 if (!client_opened(file)) 754 return 0; 755 756 offset_t position, current = file_data_current_position(file), change; 757 758 switch (whence) 759 { 760 case SEEK_SET: 761 position = offset; 762 break; 763 764 case SEEK_CUR: 765 position = current + offset; 766 break; 767 768 case SEEK_END: 769 position = file->size + offset; 770 break; 771 772 default: 773 /* NOTE: Set errno to EINVAL. */ 774 return current; 775 } 776 777 /* Retain the current position if unchanged. */ 778 779 if (position == current) 780 return position; 781 782 /* Move forward in the file. */ 783 784 if (position > current) 785 { 786 change = position - current; 787 788 /* Move towards the end of available data. 789 Request new data if not enough is available. */ 790 791 if (change <= file_data_available(file)) 792 { 793 file->data_current += change; 794 return position; 795 } 796 } 797 798 /* Move backward in the file. */ 799 800 else 801 { 802 change = current - position; 803 804 /* Move towards the start of available data. 805 Request new data if moving beyond the start of the data. */ 806 807 if (change <= file->data_current) 808 { 809 file->data_current -= change; 810 return position; 811 } 812 } 813 814 /* Handle unwritten data and reset the buffer for reading. */ 815 816 if (_access(file, position)) 817 return current; 818 819 return position; 820 } 821 822 823 824 /* Set or unset blocking access for a file. */ 825 826 long client_set_blocking(file_t *file, notify_flags_t flags) 827 { 828 long err; 829 830 if (file->can_block == flags) 831 return L4_EOK; 832 833 /* Since blocking access is used with specific file notifications, the 834 per-task notifier is used. */ 835 836 notifier_t *notifier = client_notifier_task(); 837 838 if (flags) 839 err = client_subscribe(file, flags, notifier); 840 else 841 err = client_unsubscribe(file, notifier); 842 843 if (err) 844 return err; 845 846 file->can_block = flags; 847 return L4_EOK; 848 } 849 850 851 852 /* Subscribe to events concerning a file. */ 853 854 long client_subscribe(file_t *file, notify_flags_t flags, notifier_t *notifier) 855 { 856 if (!client_opened(file)) 857 return -L4_EINVAL; 858 859 return notify_subscribe(file_notifiable(file), flags, notifier); 860 } 861 862 863 864 /* Return the current position in the file. */ 865 866 offset_t client_tell(file_t *file) 867 { 868 if (!client_opened(file)) 869 return -L4_EINVAL; 870 871 return file_data_current_position(file); 872 } 873 874 875 876 /* Unsubscribe from events concerning a file. */ 877 878 long client_unsubscribe(file_t *file, notifier_t *notifier) 879 { 880 if (!client_opened(file)) 881 return -L4_EINVAL; 882 883 return notify_unsubscribe(file_notifiable(file), notifier); 884 } 885 886 887 888 /* Wait for events involving a specific file. */ 889 890 long client_wait_file(file_t *file, notifier_t *notifier) 891 { 892 if (!client_opened(file)) 893 return -L4_EINVAL; 894 895 return file_notify_wait_file(file, notifier); 896 } 897 898 /* Wait for events concerning files, referencing a file object if an event is 899 delivered. */ 900 901 long client_wait_files(file_t **file, notifier_t *notifier) 902 { 903 return file_notify_wait_files(file, notifier); 904 } 905 906 907 908 /* Write to the filesystem object from the buffer provided. */ 909 910 offset_t client_write(file_t *file, const void *buf, offset_t count) 911 { 912 if (!client_opened(file)) 913 return 0; 914 915 /* Map memory if none has been mapped so far. */ 916 917 if (_map_memory(file, count) == NULL) 918 return 0; 919 920 /* Attempt to ensure that the file can accept the amount of data to be 921 written. This may not resize to the needed amount if a file has a fixed 922 size, but data will still be written to any available space. */ 923 924 offset_t needed_size = file_data_current_position(file) + count; 925 926 if (file->object_flags & OBJECT_HAS_SIZE) 927 { 928 if (file->size < needed_size) 929 { 930 file_resize(file, needed_size); 931 932 if (file->size < needed_size) 933 count = file->size - file_data_current_position(file); 934 } 935 } 936 937 /* Space remaining in the descriptor buffer. */ 938 939 offset_t space = file_data_space(file); 940 offset_t to_transfer, total = 0; 941 942 while (count > 0) 943 { 944 /* If no space is available, try and send data, reset the buffer. */ 945 946 if (!space) 947 { 948 /* Flush any unwritten data and continue writing from the current data 949 position. */ 950 951 if (!_access_blocking(file, file_data_current_position(file), 0)) 952 break; 953 954 space = file_data_space(file); 955 } 956 957 /* Transfer data into the supplied buffer. */ 958 959 to_transfer = space <= count ? space : count; 960 961 file_data_write(file, (char *) buf, to_transfer); 962 963 /* Update counters. */ 964 965 space -= to_transfer; 966 967 count -= to_transfer; 968 total += to_transfer; 969 970 buf = ((char *) buf + to_transfer); 971 } 972 973 return total; 974 } 975 976 // vim: tabstop=2 expandtab shiftwidth=2