source: trunk/libtransmission/fastresume.c @ 3969

Last change on this file since 3969 was 3969, checked in by charles, 15 years ago

fix crash when loading fastresume files

  • Property svn:keywords set to Date Rev Author Id
File size: 20.0 KB
Line 
1/******************************************************************************
2 * $Id: fastresume.c 3969 2007-11-26 03:54:20Z charles $
3 *
4 * Copyright (c) 2005-2007 Transmission authors and contributors
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a
7 * copy of this software and associated documentation files (the "Software"),
8 * to deal in the Software without restriction, including without limitation
9 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
10 * and/or sell copies of the Software, and to permit persons to whom the
11 * Software is furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 * DEALINGS IN THE SOFTWARE.
23 *****************************************************************************/
24
25/***********************************************************************
26 * Fast resume
27 ***********************************************************************
28 * The format of the resume file is a 4 byte format version (currently 1),
29 * followed by several variable-sized blocks of data.  Each block is
30 * preceded by a 1 byte ID and a 4 byte length.  The currently recognized
31 * IDs are defined below by the FR_ID_* macros.  The length does not include
32 * the 5 bytes for the ID and length.
33 *
34 * The name of the resume file is "resume.<hash>-<tag>", although
35 * older files with a name of "resume.<hash>" will be recognized if
36 * the former doesn't exist.
37 *
38 * All values are stored in the native endianness. Moving a
39 * libtransmission resume file from an architecture to another will not
40 * work, although it will not hurt either (the version will be wrong,
41 * so the resume file will not be read).
42 **********************************************************************/
43
44#include <assert.h>
45#include <errno.h>
46#include <stdio.h>
47#include <stdlib.h>
48#include <string.h>
49#include <time.h>
50
51#include <sys/types.h>
52#include <sys/stat.h>
53#include <unistd.h>
54
55#include <event.h>
56
57#include "transmission.h"
58#include "completion.h"
59#include "fastresume.h"
60#include "peer-mgr.h"
61#include "platform.h"
62#include "utils.h"
63
64/* time_t can be 32 or 64 bits... for consistency we'll hardwire 64 */ 
65typedef uint64_t tr_time_t; 
66
67enum
68{
69    /* deprecated */
70    FR_ID_PROGRESS_SLOTS = 1,
71
72    /* number of bytes downloaded */
73    FR_ID_DOWNLOADED = 2,
74
75    /* number of bytes uploaded */
76    FR_ID_UPLOADED = 3,
77
78    /* IPs and ports of connectable peers */
79    FR_ID_PEERS_OLD = 4,
80
81    /* progress data:
82     *  - 4 bytes * number of files: mtimes of files
83     *  - 1 bit * number of blocks: whether we have the block or not */
84    FR_ID_PROGRESS = 5,
85
86    /* dnd and priority
87     * char * number of files: l,n,h for low, normal, high priority
88     * char * number of files: t,f for DND flags */
89    FR_ID_PRIORITY = 6,
90
91    /* transfer speeds
92     * uint32_t: dl speed rate to use when the mode is single
93     * uint32_t: dl's tr_speedlimit
94     * uint32_t: ul speed rate to use when the mode is single
95     * uint32_t: ul's tr_speedlimit
96     */
97    FR_ID_SPEED = 8,
98
99    /* active
100     * char: 't' if running, 'f' if paused
101     */
102    FR_ID_RUN = 9,
103
104    /* number of corrupt bytes downloaded */
105    FR_ID_CORRUPT = 10,
106
107    /* IPs and ports of connectable peers */
108    FR_ID_PEERS = 11,
109
110    /* destination of the torrent: zero-terminated string */
111    FR_ID_DESTINATION = 12,
112
113    /* pex flag
114     * 't' if pex is enabled, 'f' if disabled */
115    FR_ID_PEX = 13
116};
117
118
119/* macros for the length of various pieces of the progress data */
120#define FR_MTIME_LEN( t ) \
121  ( sizeof(tr_time_t) * (t)->info.fileCount )
122#define FR_BLOCK_BITFIELD_LEN( t ) \
123  ( ( (t)->blockCount + 7 ) / 8 )
124#define FR_PROGRESS_LEN( t ) \
125  ( FR_MTIME_LEN( t ) + FR_BLOCK_BITFIELD_LEN( t ) )
126#define FR_SPEED_LEN (2 * (sizeof(uint16_t) + sizeof(uint8_t) ) )
127
128static void
129fastResumeFileName( char * buf, size_t buflen, const tr_torrent * tor, int tag )
130{
131    const char * cacheDir = tr_getCacheDirectory ();
132    const char * hash = tor->info.hashString;
133
134    if( !tag )
135    {
136        tr_buildPath( buf, buflen, cacheDir, hash, NULL );
137    }
138    else
139    {
140        char base[1024];
141        snprintf( base, sizeof(base), "%s-%s", hash, tor->handle->tag );
142        tr_buildPath( buf, buflen, cacheDir, base, NULL );
143    }
144}
145
146static tr_time_t*
147getMTimes( const tr_torrent * tor, int * setme_n )
148{
149    int i;
150    const int n = tor->info.fileCount;
151    tr_time_t * m = calloc( n, sizeof(tr_time_t) );
152
153    for( i=0; i<n; ++i ) {
154        char fname[MAX_PATH_LENGTH];
155        struct stat sb;
156        tr_buildPath( fname, sizeof(fname),
157                      tor->destination, tor->info.files[i].name, NULL );
158        if ( !stat( fname, &sb ) && S_ISREG( sb.st_mode ) ) {
159#ifdef SYS_DARWIN
160            m[i] = sb.st_mtimespec.tv_sec;
161#else
162            m[i] = sb.st_mtime;
163#endif
164        }
165    }
166
167    *setme_n = n;
168    return m;
169}
170
171static void
172fastResumeWriteData( uint8_t       id,
173                     const void  * data,
174                     uint32_t      size,
175                     uint32_t      count,
176                     FILE        * file )
177{
178    uint32_t  datalen = size * count;
179
180    fwrite( &id, 1, 1, file );
181    fwrite( &datalen, 4, 1, file );
182    fwrite( data, size, count, file );
183}
184
185void
186tr_fastResumeSave( const tr_torrent * tor )
187{
188    char      path[MAX_PATH_LENGTH];
189    FILE    * file;
190    const int version = 1;
191    uint64_t  total;
192
193    fastResumeFileName( path, sizeof path, tor, 1 );
194    file = fopen( path, "wb+" );
195    if( NULL == file ) {
196        tr_err( "Couldn't open '%s' for writing", path );
197        return;
198    }
199   
200    /* Write format version */
201    fwrite( &version, 4, 1, file );
202
203    if( TRUE ) /* FR_ID_DESTINATION */
204    {
205        const char * d = tor->destination ? tor->destination : "";
206        const int byteCount = strlen( d ) + 1;
207        fastResumeWriteData( FR_ID_DESTINATION, d, 1, byteCount, file );
208    }
209
210    /* Write progress data */
211    if (1) {
212        int n;
213        tr_time_t * mtimes;
214        uint8_t * buf = malloc( FR_PROGRESS_LEN( tor ) );
215        uint8_t * walk = buf;
216        const tr_bitfield * bitfield;
217
218        /* mtimes */
219        mtimes = getMTimes( tor, &n );
220        memcpy( walk, mtimes, n*sizeof(tr_time_t) );
221        walk += n * sizeof(tr_time_t);
222
223        /* completion bitfield */
224        bitfield = tr_cpBlockBitfield( tor->completion );
225        assert( (unsigned)FR_BLOCK_BITFIELD_LEN( tor ) == bitfield->len );
226        memcpy( walk, bitfield->bits, bitfield->len );
227        walk += bitfield->len;
228
229        /* write it */
230        assert( walk-buf == (int)FR_PROGRESS_LEN( tor ) );
231        fastResumeWriteData( FR_ID_PROGRESS, buf, 1, walk-buf, file );
232
233        /* cleanup */
234        free( mtimes );
235        free( buf );
236    }
237
238
239    /* Write the priorities and DND flags */
240    if( TRUE )
241    {
242        int i;
243        const int n = tor->info.fileCount;
244        char * buf = tr_new0( char, n*2 );
245        char * walk = buf;
246
247        /* priorities */
248        for( i=0; i<n; ++i ) {
249            char ch;
250            const int priority = tor->info.files[i].priority;
251            switch( priority ) {
252               case TR_PRI_LOW:   ch = 'l'; break; /* low */
253               case TR_PRI_HIGH:  ch = 'h'; break; /* high */
254               default:           ch = 'n'; break; /* normal */
255            };
256            *walk++ = ch;
257        }
258
259        /* dnd flags */
260        for( i=0; i<n; ++i )
261            *walk++ = tor->info.files[i].dnd ? 't' : 'f';
262
263        /* write it */
264        assert( walk - buf == 2*n );
265        fastResumeWriteData( FR_ID_PRIORITY, buf, 1, walk-buf, file );
266
267        /* cleanup */
268        tr_free( buf );
269    }
270
271
272    /* Write the torrent ul/dl speed caps */
273    if( TRUE )
274    {
275        const int len = FR_SPEED_LEN;
276        char * buf = tr_new0( char, len );
277        char * walk = buf;
278        uint16_t i16;
279        uint8_t i8;
280
281        i16 = (uint16_t) tr_torrentGetSpeedLimit( tor, TR_DOWN );
282        memcpy( walk, &i16, 2 ); walk += 2;
283        i8 = (uint8_t) tr_torrentGetSpeedMode( tor, TR_DOWN );
284        memcpy( walk, &i8, 1 ); walk += 1;
285        i16 = (uint16_t) tr_torrentGetSpeedLimit( tor, TR_UP );
286        memcpy( walk, &i16, 2 ); walk += 2;
287        i8 = (uint8_t) tr_torrentGetSpeedMode( tor, TR_UP );
288        memcpy( walk, &i8, 1 ); walk += 1;
289
290        assert( walk - buf == len );
291        fastResumeWriteData( FR_ID_SPEED, buf, 1, walk-buf, file );
292        tr_free( buf );
293    }
294
295    if( TRUE ) /* FR_ID_PEX */
296    {
297        const char flag = tor->pexDisabled ? 'f' : 't';
298        fastResumeWriteData( FR_ID_PEX, &flag, 1, 1, file );
299    }
300
301    if( TRUE ) /* FR_ID_RUN */
302    {
303        const char is_running = tor->isRunning ? 't' : 'f';
304        fastResumeWriteData( FR_ID_RUN, &is_running, 1, 1, file );
305    }
306
307    /* Write download and upload totals */
308
309    total = tor->downloadedCur + tor->downloadedPrev;
310    fastResumeWriteData( FR_ID_DOWNLOADED, &total, 8, 1, file );
311
312    total = tor->uploadedCur + tor->uploadedPrev;
313    fastResumeWriteData( FR_ID_UPLOADED, &total, 8, 1, file );
314
315    total = tor->corruptCur + tor->corruptPrev;
316    fastResumeWriteData( FR_ID_CORRUPT, &total, 8, 1, file );
317
318    if( !tor->info.isPrivate )
319    {
320        tr_pex * pex;
321        const int count = tr_peerMgrGetPeers( tor->handle->peerMgr,
322                                              tor->info.hash,
323                                              &pex );
324        if( count > 0 )
325            fastResumeWriteData( FR_ID_PEERS, pex, sizeof(tr_pex), count, file );
326        tr_free( pex );
327    }
328
329    fclose( file );
330
331    tr_dbg( "Resume file '%s' written", path );
332}
333
334/***
335****
336***/
337
338static uint64_t
339internalIdToPublicBitfield( uint8_t id )
340{
341    uint64_t ret = 0;
342
343    switch( id )
344    {
345        case FR_ID_PROGRESS_SLOTS: ret = 0;                 break;
346        case FR_ID_DOWNLOADED:     ret = TR_FR_DOWNLOADED;  break;
347        case FR_ID_UPLOADED:       ret = TR_FR_UPLOADED;    break;
348        case FR_ID_PEERS_OLD:      ret = TR_FR_PEERS;       break;
349        case FR_ID_PROGRESS:       ret = TR_FR_PROGRESS;    break;
350        case FR_ID_PRIORITY:       ret = TR_FR_PRIORITY;    break;
351        case FR_ID_SPEED:          ret = TR_FR_SPEEDLIMIT;  break;
352        case FR_ID_RUN:            ret = TR_FR_RUN;         break;
353        case FR_ID_CORRUPT:        ret = TR_FR_CORRUPT;     break;
354        case FR_ID_PEERS:          ret = TR_FR_PEERS;       break;
355        case FR_ID_DESTINATION:    ret = TR_FR_DESTINATION; break;
356        case FR_ID_PEX:            ret = TR_FR_PEX;         break;
357    }
358
359    return ret;
360}
361
362static void
363readBytes( void * target, const uint8_t ** source, size_t byteCount )
364{
365    memcpy( target, *source, byteCount );
366    *source += byteCount;
367}
368
369static uint64_t
370parseDownloaded( tr_torrent * tor, const uint8_t * buf, uint32_t len )
371{
372    if( len != sizeof(uint64_t) )
373        return 0;
374    readBytes( &tor->downloadedPrev, &buf, sizeof(uint64_t) );
375    return TR_FR_DOWNLOADED;
376}
377
378static uint64_t
379parseUploaded( tr_torrent * tor, const uint8_t * buf, uint32_t len )
380{
381    if( len != sizeof(uint64_t) )
382        return 0;
383    readBytes( &tor->uploadedPrev, &buf, sizeof(uint64_t) );
384    return TR_FR_UPLOADED;
385}
386
387static uint64_t
388parseCorrupt( tr_torrent * tor, const uint8_t * buf, uint32_t len )
389{
390    if( len != sizeof(uint64_t) )
391        return 0;
392    readBytes( &tor->corruptPrev, &buf, sizeof(uint64_t) );
393    return TR_FR_CORRUPT;
394}
395
396static uint64_t
397parseProgress( const tr_torrent  * tor,
398               const uint8_t     * buf,
399               uint32_t            len,
400               tr_bitfield       * uncheckedPieces )
401{
402    int i;
403    uint64_t ret = 0;
404   
405    if( len == FR_PROGRESS_LEN( tor ) )
406    {
407        int n;
408        tr_bitfield bitfield;
409
410        /* compare file mtimes */
411        tr_time_t * curMTimes = getMTimes( tor, &n );
412        const uint8_t * walk = buf;
413        const tr_time_t * oldMTimes = (const tr_time_t *) walk;
414        for( i=0; i<n; ++i ) {
415            if ( !curMTimes[i] || ( curMTimes[i] != oldMTimes[i] ) ) {
416                const tr_file * file = &tor->info.files[i];
417                tr_dbg( "File '%s' mtimes differ-- flagging pieces [%d..%d] for recheck",
418                        file->name, file->firstPiece, file->lastPiece);
419                tr_bitfieldAddRange( uncheckedPieces, 
420                                     file->firstPiece, file->lastPiece+1 );
421            }
422        }
423        free( curMTimes );
424        walk += n * sizeof(tr_time_t);
425
426        /* get the completion bitfield */
427        memset( &bitfield, 0, sizeof bitfield );
428        bitfield.len = FR_BLOCK_BITFIELD_LEN( tor );
429        bitfield.bits = (uint8_t*) walk;
430        tr_cpBlockBitfieldSet( tor->completion, &bitfield );
431
432        ret = TR_FR_PROGRESS;
433    }
434
435    /* the files whose mtimes are wrong,
436       remove from completion pending a recheck... */
437    for( i=0; i<tor->info.pieceCount; ++i )
438        if( tr_bitfieldHas( uncheckedPieces, i ) )
439            tr_cpPieceRem( tor->completion, i );
440
441    return ret;
442}
443
444static uint64_t
445parsePriorities( tr_torrent * tor, const uint8_t * buf, uint32_t len )
446{
447    uint64_t ret = 0;
448
449    if( len == (uint32_t)(2 * tor->info.fileCount) )
450    {
451        const size_t n = tor->info.fileCount;
452        const size_t len = 2 * n;
453        int *dnd = NULL, dndCount = 0;
454        int *dl = NULL, dlCount = 0;
455        size_t i;
456        const uint8_t * walk = buf;
457
458        /* set file priorities */
459        for( i=0; i<n; ++i ) {
460           tr_priority_t priority;
461           const char ch = *walk++;
462           switch( ch ) {
463               case 'l': priority = TR_PRI_LOW; break;
464               case 'h': priority = TR_PRI_HIGH; break;
465               default:  priority = TR_PRI_NORMAL; break;
466           }
467           tor->info.files[i].priority = priority;
468        }
469
470        /* set the dnd flags */
471        dl = tr_new( int, len );
472        dnd = tr_new( int, len );
473        for( i=0; i<n; ++i )
474            if( *walk++ == 't' ) /* 't' means the DND flag is true */
475                dnd[dndCount++] = i;
476            else
477                dl[dlCount++] = i;
478
479        if( dndCount )
480            tr_torrentSetFileDLs ( tor, dnd, dndCount, FALSE );
481        if( dlCount )
482            tr_torrentSetFileDLs ( tor, dl, dlCount, TRUE );
483
484        tr_free( dnd );
485        tr_free( dl );
486
487        ret = TR_FR_PRIORITY;
488    }
489
490    return ret;
491}
492
493static uint64_t
494parseSpeedLimit( tr_torrent * tor, const uint8_t * buf, uint32_t len )
495{
496    uint64_t ret = 0;
497
498    if( len == FR_SPEED_LEN )
499    {
500        uint8_t i8;
501        uint16_t i16;
502
503        readBytes( &i16, &buf, sizeof(i16) );
504        tr_torrentSetSpeedLimit( tor, TR_DOWN, i16 );
505        readBytes( &i8, &buf, sizeof(i8) );
506        tr_torrentSetSpeedMode( tor, TR_DOWN, (tr_speedlimit)i8 );
507        readBytes( &i16, &buf, sizeof(i16) );
508        tr_torrentSetSpeedLimit( tor, TR_UP, i16 );
509        readBytes( &i8, &buf, sizeof(i8) );
510        tr_torrentSetSpeedMode( tor, TR_UP, (tr_speedlimit)i8 );
511
512        ret = TR_FR_SPEEDLIMIT;
513    }
514
515    return ret;
516}
517
518static uint64_t
519parseRun( tr_torrent * tor, const uint8_t * buf, uint32_t len )
520{
521    if( len != 1 )
522        return 0;
523    tor->isRunning = *buf=='t';
524    return TR_FR_RUN;
525}
526
527static uint64_t
528parsePex( tr_torrent * tor, const uint8_t * buf, uint32_t len )
529{
530    if( len != 1 )
531        return 0;
532    tor->pexDisabled = *buf!='t';
533    return TR_FR_PEX;
534}
535
536static uint64_t
537parsePeers( tr_torrent * tor, const uint8_t * buf, uint32_t len )
538{
539    uint64_t ret = 0;
540
541    if( !tor->info.isPrivate )
542    {
543        const int count = len / sizeof(tr_pex);
544        tr_peerMgrAddPex( tor->handle->peerMgr,
545                          tor->info.hash,
546                          TR_PEER_FROM_CACHE,
547                          (tr_pex*)buf, count );
548        tr_dbg( "found %i peers in resume file", count );
549        ret = TR_FR_PEERS;
550    }
551
552    return ret;
553}
554
555static uint64_t
556parseDestination( tr_torrent * tor, const uint8_t * buf, uint32_t len,
557                  const char * destination, int argIsFallback )
558{
559    if( argIsFallback )
560        tor->destination = tr_strdup( len ? (const char*)buf : destination );
561    else
562        tor->destination = tr_strdup( len ? destination : (const char*)buf );
563
564    return TR_FR_DESTINATION;
565}
566
567static uint64_t
568parseVersion1( tr_torrent * tor, const uint8_t * buf, const uint8_t * end,
569               uint64_t fieldsToLoad,
570               tr_bitfield  * uncheckedPieces,
571               const char * destination, int argIsFallback )
572{
573    uint64_t ret = 0;
574
575    while( end-buf >= 5 )
576    {
577        uint8_t id;
578        uint32_t len;
579        readBytes( &id, &buf, sizeof(id) );
580        readBytes( &len, &buf, sizeof(len) );
581
582        if( fieldsToLoad & internalIdToPublicBitfield( id ) ) switch( id )
583        {
584            case FR_ID_DOWNLOADED:  ret |= parseDownloaded( tor, buf, len ); break;
585            case FR_ID_UPLOADED:    ret |= parseUploaded( tor, buf, len ); break;
586            case FR_ID_PROGRESS:    ret |= parseProgress( tor, buf, len, uncheckedPieces ); break;
587            case FR_ID_PRIORITY:    ret |= parsePriorities( tor, buf, len ); break;
588            case FR_ID_SPEED:       ret |= parseSpeedLimit( tor, buf, len ); break;
589            case FR_ID_RUN:         ret |= parseRun( tor, buf, len ); break;
590            case FR_ID_CORRUPT:     ret |= parseCorrupt( tor, buf, len ); break;
591            case FR_ID_PEERS:       ret |= parsePeers( tor, buf, len ); break;
592            case FR_ID_PEX:         ret |= parsePex( tor, buf, len ); break;
593            case FR_ID_DESTINATION: ret |= parseDestination( tor, buf, len, destination, argIsFallback ); break;
594            default:                tr_dbg( "Skipping unknown resume code %d", (int)id ); break;
595        }
596
597        buf += len;
598    }
599
600    return ret;
601}
602
603static uint8_t* 
604loadResumeFile( const tr_torrent * tor, size_t * len )
605{
606    uint8_t * ret = NULL;
607    char path[MAX_PATH_LENGTH];
608    const char * cacheDir = tr_getCacheDirectory ();
609    const char * hash = tor->info.hashString;
610
611    if( !ret && tor->handle->tag )
612    {
613        char base[1024];
614        snprintf( base, sizeof(base), "%s-%s", hash, tor->handle->tag );
615        tr_buildPath( path, sizeof(path), cacheDir, base, NULL );
616        ret = tr_loadFile( path, len );
617    }
618
619    if( !ret )
620    {
621        tr_buildPath( path, sizeof(path), cacheDir, hash, NULL );
622        ret = tr_loadFile( path, len );
623    }
624
625    return ret;
626}
627
628static uint64_t
629fastResumeLoadImpl ( tr_torrent   * tor,
630                     uint64_t       fieldsToLoad,
631                     tr_bitfield  * uncheckedPieces,
632                     const char   * destination,
633                     int            argIsFallback )
634{
635    uint64_t ret = 0;
636    size_t size = 0;
637    uint8_t * buf = loadResumeFile( tor, &size );
638
639    if( !buf )
640        tr_inf( "Couldn't read resume file for '%s'", tor->info.name );
641    else {
642        const uint8_t * walk = buf;
643        const uint8_t * end = walk + size;
644        if( end - walk >= 4 ) {
645            uint32_t version;
646            readBytes( &version, &walk, sizeof(version) );
647            if( version == 1 )
648                ret |= parseVersion1 ( tor, walk, end, fieldsToLoad, uncheckedPieces, destination, argIsFallback );
649            else
650                tr_inf( "Unsupported resume file %d for '%s'", version, tor->info.name );
651        }
652
653        tr_free( buf );
654    }
655
656    return ret;
657}
658
659uint64_t
660tr_fastResumeLoad( tr_torrent   * tor,
661                   uint64_t       fieldsToLoad,
662                   tr_bitfield  * uncheckedPieces,
663                   const char   * destination,
664                   int            argIsFallback )
665{
666    const uint64_t ret = fastResumeLoadImpl( tor, fieldsToLoad, uncheckedPieces, destination, argIsFallback );
667
668    if( ! ( ret & TR_FR_PROGRESS ) )
669        tr_bitfieldAddRange( uncheckedPieces, 0, tor->info.pieceCount );
670
671    if( !tor->destination )
672        tor->destination = tr_strdup( destination );
673
674    return ret;
675}
Note: See TracBrowser for help on using the repository browser.