source: trunk/libtransmission/utils.c @ 4213

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

fix an obscure bug in tr_torrentStat() where (% verified + % unverified + % unavailable) could be > 1.0

  • Property svn:keywords set to Date Rev Author Id
File size: 18.6 KB
Line 
1/******************************************************************************
2 * $Id: utils.c 4213 2007-12-19 05:57:55Z 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#include <assert.h>
26#include <ctype.h>
27#include <errno.h>
28#include <stdarg.h>
29#include <stdio.h>
30#include <stdlib.h>
31#include <string.h>
32
33#include <sys/time.h>
34#include <sys/types.h>
35#include <sys/stat.h>
36#include <unistd.h> /* usleep, stat */
37
38#include "event.h"
39
40#ifdef WIN32
41    #include <windows.h> /* for Sleep */
42#elif defined(__BEOS__)
43    #include <kernel/OS.h>
44#endif
45
46#include "transmission.h"
47#include "trcompat.h"
48#include "utils.h"
49#include "platform.h"
50
51static tr_lock      * messageLock = NULL;
52static int            messageLevel = 0;
53static int            messageQueuing = FALSE;
54static tr_msg_list *  messageQueue = NULL;
55static tr_msg_list ** messageQueueTail = &messageQueue;
56
57void tr_msgInit( void )
58{
59    if( !messageLock )
60         messageLock = tr_lockNew( );
61}
62
63FILE*
64tr_getLog( void )
65{
66    static int initialized = FALSE;
67    static FILE * file= NULL;
68
69    if( !initialized )
70    {
71        const char * str = getenv( "TR_DEBUG_FD" );
72        int fd;
73        if( str && *str )
74            fd = atoi( str );
75        switch( fd ) {
76            case 1: file = stdout; break;
77            case 2: file = stderr; break;
78            default: file = NULL; break;
79        }
80        initialized = TRUE;
81    }
82
83    return file;
84}
85
86char*
87tr_getLogTimeStr( char * buf, int buflen )
88{
89    char tmp[64];
90    time_t now;
91    struct tm now_tm;
92    struct timeval tv;
93    int milliseconds;
94
95    now = time( NULL );
96    gettimeofday( &tv, NULL );
97
98    localtime_r( &now, &now_tm );
99    strftime( tmp, sizeof(tmp), "%H:%M:%S", &now_tm );
100    milliseconds = (int)(tv.tv_usec / 1000);
101    snprintf( buf, buflen, "%s.%03d", tmp, milliseconds );
102
103    return buf;
104}
105
106void
107tr_setMessageLevel( int level )
108{
109    tr_msgInit();
110    tr_lockLock( messageLock );
111    messageLevel = MAX( 0, level );
112    tr_lockUnlock( messageLock );
113}
114
115int tr_getMessageLevel( void )
116{
117    int ret;
118
119    tr_msgInit();
120    tr_lockLock( messageLock );
121    ret = messageLevel;
122    tr_lockUnlock( messageLock );
123
124    return ret;
125}
126
127void tr_setMessageQueuing( int enabled )
128{
129    tr_msgInit();
130    tr_lockLock( messageLock );
131    messageQueuing = enabled;
132    tr_lockUnlock( messageLock );
133}
134
135tr_msg_list * tr_getQueuedMessages( void )
136{
137    tr_msg_list * ret;
138
139    assert( NULL != messageLock );
140    tr_lockLock( messageLock );
141    ret = messageQueue;
142    messageQueue = NULL;
143    messageQueueTail = &messageQueue;
144    tr_lockUnlock( messageLock );
145
146    return ret;
147}
148
149void tr_freeMessageList( tr_msg_list * list )
150{
151    tr_msg_list * next;
152
153    while( NULL != list )
154    {
155        next = list->next;
156        free( list->message );
157        free( list );
158        list = next;
159    }
160}
161
162int
163tr_vasprintf( char **strp, const char *fmt, va_list ap )
164{
165    int ret;
166    struct evbuffer * buf = evbuffer_new( );
167    *strp = NULL;
168    if( evbuffer_add_vprintf( buf, fmt, ap ) < 0 )
169        ret = -1;
170    else {
171        ret = EVBUFFER_LENGTH( buf );
172        *strp = tr_strndup( (char*)EVBUFFER_DATA(buf), ret );
173    }
174    evbuffer_free( buf );
175    return ret;
176
177}
178
179int
180tr_asprintf( char **strp, const char *fmt, ...)
181{
182    int ret;
183    va_list ap;
184    va_start( ap, fmt );
185    ret = tr_vasprintf( strp, fmt, ap );
186    va_end( ap );
187    return ret;
188}
189
190void
191tr_msg( const char * file, int line, int level, const char * fmt, ... )
192{
193    FILE * fp;
194
195    assert( NULL != messageLock );
196    tr_lockLock( messageLock );
197
198    fp = tr_getLog( );
199
200    if( !messageLevel )
201    {
202        char * env = getenv( "TR_DEBUG" );
203        messageLevel = ( env ? atoi( env ) : 0 ) + 1;
204        messageLevel = MAX( 1, messageLevel );
205    }
206
207    if( messageLevel >= level )
208    {
209        va_list ap;
210        char * text;
211
212        /* build the text message */
213        va_start( ap, fmt );
214        tr_vasprintf( &text, fmt, ap );
215        va_end( ap );
216
217        if( text != NULL )
218        {
219            if( messageQueuing )
220            {
221                tr_msg_list * newmsg;
222                newmsg = tr_new0( tr_msg_list, 1 );
223                newmsg->level = level;
224                newmsg->when = time( NULL );
225                newmsg->message = text;
226                newmsg->file = file;
227                newmsg->line = line;
228
229                *messageQueueTail = newmsg;
230                messageQueueTail = &newmsg->next;
231            }
232            else
233            {
234                if( fp == NULL )
235                    fp = stderr;
236                fprintf( stderr, "%s\n", text );
237                tr_free( text );
238                fflush( fp );
239            }
240        }
241    }
242
243    tr_lockUnlock( messageLock );
244}
245
246int tr_rand( int sup )
247{
248    static int init = 0;
249
250    assert( sup > 0 );
251
252    if( !init )
253    {
254        srand( tr_date() );
255        init = 1;
256    }
257    return rand() % sup;
258}
259
260/***
261****
262***/
263
264void
265tr_set_compare( const void * va, size_t aCount,
266                const void * vb, size_t bCount,
267                int compare( const void * a, const void * b ),
268                size_t elementSize,
269                tr_set_func in_a_cb,
270                tr_set_func in_b_cb,
271                tr_set_func in_both_cb,
272                void * userData )
273{
274    const uint8_t * a = (const uint8_t *) va;
275    const uint8_t * b = (const uint8_t *) vb;
276    const uint8_t * aend = a + elementSize*aCount;
277    const uint8_t * bend = b + elementSize*bCount;
278
279    while( a!=aend || b!=bend )
280    {
281        if( a==aend )
282        {
283            (*in_b_cb)( (void*)b, userData );
284            b += elementSize;
285        }
286        else if ( b==bend )
287        {
288            (*in_a_cb)( (void*)a, userData );
289            a += elementSize;
290        }
291        else
292        {
293            const int val = (*compare)( a, b );
294
295            if( !val )
296            {
297                (*in_both_cb)( (void*)a, userData );
298                a += elementSize;
299                b += elementSize;
300            }
301            else if( val < 0 )
302            {
303                (*in_a_cb)( (void*)a, userData );
304                a += elementSize;
305            }
306            else if( val > 0 )
307            {
308                (*in_b_cb)( (void*)b, userData );
309                b += elementSize;
310            }
311        }
312    }
313}
314
315/***
316****
317***/
318
319int
320tr_compareUint16( uint16_t a, uint16_t b )
321{
322    if( a < b ) return -1;
323    if( a > b ) return 1;
324    return 0;
325}
326
327int
328tr_compareUint32( uint32_t a, uint32_t b )
329{
330    if( a < b ) return -1;
331    if( a > b ) return 1;
332    return 0;
333}
334
335/**
336***
337**/
338
339struct timeval
340timevalMsec( uint64_t milliseconds )
341{
342    struct timeval ret;
343    const uint64_t microseconds = milliseconds * 1000;
344    ret.tv_sec  = microseconds / 1000000;
345    ret.tv_usec = microseconds % 1000000;
346    return ret;
347}
348
349uint8_t *
350tr_loadFile( const char * path, size_t * size )
351{
352    uint8_t    * buf;
353    struct stat  sb;
354    FILE       * file;
355
356    /* try to stat the file */
357    errno = 0;
358    if( stat( path, &sb ) )
359    {
360        tr_err( "Couldn't get information for file \"%s\" %s", path, strerror(errno) );
361        return NULL;
362    }
363
364    if( ( sb.st_mode & S_IFMT ) != S_IFREG )
365    {
366        tr_err( "Not a regular file (%s)", path );
367        return NULL;
368    }
369
370    /* Load the torrent file into our buffer */
371    file = fopen( path, "rb" );
372    if( !file )
373    {
374        tr_err( "Couldn't open file \"%s\" %s", path, strerror(errno) );
375        return NULL;
376    }
377    buf = malloc( sb.st_size );
378    if( NULL == buf )
379    {
380        tr_err( "Couldn't allocate memory (%"PRIu64" bytes)",
381                ( uint64_t )sb.st_size );
382        fclose( file );
383    }
384    fseek( file, 0, SEEK_SET );
385    if( fread( buf, sb.st_size, 1, file ) != 1 )
386    {
387        tr_err( "Error reading \"%s\" %s", path, strerror(errno) );
388        free( buf );
389        fclose( file );
390        return NULL;
391    }
392    fclose( file );
393
394    *size = sb.st_size;
395
396    return buf;
397}
398
399int
400tr_mkdir( const char * path, int permissions
401#ifdef WIN32
402                                             UNUSED
403#endif
404                                                    )
405{
406#ifdef WIN32
407    return mkdir( path );
408#else
409    return mkdir( path, permissions );
410#endif
411}
412
413int
414tr_mkdirp( const char * path_in, int permissions )
415{
416    char * path = tr_strdup( path_in );
417    char * p, * pp;
418    struct stat sb;
419    int done;
420
421    /* walk past the root */
422    p = path;
423    while( *p == TR_PATH_DELIMITER )
424        ++p;
425
426    pp = p;
427    done = 0;
428    while( ( p = strchr( pp, TR_PATH_DELIMITER ) ) || ( p = strchr( pp, '\0' ) ) )
429    {
430        if( !*p )
431            done = 1;
432        else
433            *p = '\0';
434
435        if( stat( path, &sb ) )
436        {
437            /* Folder doesn't exist yet */
438            if( tr_mkdir( path, permissions ) )
439            {
440                tr_err( "Could not create directory %s (%s)", path,
441                        strerror( errno ) );
442                tr_free( path );
443                return 1;
444            }
445        }
446        else if( ( sb.st_mode & S_IFMT ) != S_IFDIR )
447        {
448            /* Node exists but isn't a folder */
449            tr_err( "Remove %s, it's in the way.", path );
450            tr_free( path );
451            return 1;
452        }
453
454        if( done )
455            break;
456
457        *p = TR_PATH_DELIMITER;
458        p++;
459        pp = p;
460    }
461
462    tr_free( path );
463    return 0;
464}
465
466void
467tr_buildPath ( char *buf, size_t buflen, const char *first_element, ... )
468{
469    struct evbuffer * evbuf = evbuffer_new( );
470    const char * element = first_element;
471    va_list vl;
472    va_start( vl, first_element );
473    while( element ) {
474        if( EVBUFFER_LENGTH(evbuf) )
475            evbuffer_add_printf( evbuf, "%c", TR_PATH_DELIMITER );
476        evbuffer_add_printf( evbuf, "%s", element );
477        element = (const char*) va_arg( vl, const char* );
478    }
479    strlcpy( buf, (char*)EVBUFFER_DATA(evbuf), buflen );
480    evbuffer_free( evbuf );
481}
482
483int
484tr_ioErrorFromErrno( void )
485{
486    switch( errno )
487    {
488        case EACCES:
489        case EROFS:
490            return TR_ERROR_IO_PERMISSIONS;
491        case ENOSPC:
492            return TR_ERROR_IO_SPACE;
493        case EMFILE:
494            return TR_ERROR_IO_OPEN_FILES;
495        case EFBIG:
496            return TR_ERROR_IO_FILE_TOO_BIG;
497        default:
498            tr_dbg( "generic i/o errno from errno: %s", strerror( errno ) );
499            return TR_ERROR_IO_OTHER;
500    }
501}
502
503char *
504tr_errorString( int code )
505{
506    switch( code )
507    {
508        case TR_OK:
509            return "No error";
510        case TR_ERROR:
511            return "Generic error";
512        case TR_ERROR_ASSERT:
513            return "Assert error";
514        case TR_ERROR_IO_PERMISSIONS:
515            return "Insufficient permissions";
516        case TR_ERROR_IO_SPACE:
517            return "Insufficient free space";
518        case TR_ERROR_IO_DUP_DOWNLOAD:
519            return "Already active transfer with same name and download folder";
520        case TR_ERROR_IO_FILE_TOO_BIG:
521            return "File too large";
522        case TR_ERROR_IO_OPEN_FILES:
523            return "Too many open files";
524        case TR_ERROR_IO_OTHER:
525            return "Generic I/O error";
526    }
527    return "Unknown error";
528}
529
530/****
531*****
532****/
533
534char*
535tr_strdup( const char * in )
536{
537    return tr_strndup( in, in ? strlen(in) : 0 );
538}
539
540char*
541tr_strndup( const char * in, int len )
542{
543    char * out = NULL;
544
545    if( len < 0 )
546    {
547        out = tr_strdup( in );
548    }
549    else if( in != NULL )
550    {
551        out = tr_malloc( len+1 );
552        memcpy( out, in, len );
553        out[len] = '\0';
554    }
555    return out;
556}
557
558void*
559tr_calloc( size_t nmemb, size_t size )
560{
561    return nmemb && size ? calloc( nmemb, size ) : NULL;
562}
563
564void*
565tr_malloc( size_t size )
566{
567    return size ? malloc( size ) : NULL;
568}
569
570void*
571tr_malloc0( size_t size )
572{
573    void * ret = tr_malloc( size );
574    memset( ret, 0, size );
575    return ret;
576}
577
578void
579tr_free( void * p )
580{
581    if( p )
582        free( p );
583}
584
585/****
586*****
587****/
588
589/* note that the argument is how many bits are needed, not bytes */
590tr_bitfield*
591tr_bitfieldNew( size_t bitcount )
592{
593    tr_bitfield * ret = calloc( 1, sizeof(tr_bitfield) );
594    if( NULL == ret )
595        return NULL;
596
597    ret->len = (bitcount+7u) / 8u;
598    ret->bits = calloc( ret->len, 1 );
599    if( NULL == ret->bits ) {
600        free( ret );
601        return NULL;
602    }
603
604    return ret;
605}
606
607tr_bitfield*
608tr_bitfieldDup( const tr_bitfield * in )
609{
610    tr_bitfield * ret = calloc( 1, sizeof(tr_bitfield) );
611    ret->len = in->len;
612    ret->bits = malloc( ret->len );
613    memcpy( ret->bits, in->bits, ret->len );
614    return ret;
615}
616
617void tr_bitfieldFree( tr_bitfield * bitfield )
618{
619    if( bitfield )
620    {
621        free( bitfield->bits );
622        free( bitfield );
623    }
624}
625
626void
627tr_bitfieldClear( tr_bitfield * bitfield )
628{
629    memset( bitfield->bits, 0, bitfield->len );
630}
631
632int
633tr_bitfieldIsEmpty( const tr_bitfield * bitfield )
634{
635    unsigned int i;
636
637    for( i=0; i<bitfield->len; ++i )
638        if( bitfield->bits[i] )
639            return 0;
640
641    return 1;
642}
643
644int
645tr_bitfieldHas( const tr_bitfield * bitfield, size_t nth )
646{
647    static const uint8_t ands[8] = { 128, 64, 32, 16, 8, 4, 2, 1 };
648    return bitfield!=NULL && (bitfield->bits[nth>>3u] & ands[nth&7u] );
649}
650
651void
652tr_bitfieldAdd( tr_bitfield  * bitfield, size_t nth )
653{
654    static const uint8_t ands[8] = { 128, 64, 32, 16, 8, 4, 2, 1 };
655    bitfield->bits[nth>>3u] |= ands[nth&7u];
656    assert( tr_bitfieldHas( bitfield, nth ) );
657}
658
659void
660tr_bitfieldAddRange( tr_bitfield  * bitfield,
661                     size_t         begin,
662                     size_t         end )
663{
664    /* TODO: there are faster ways to do this */
665    unsigned int i;
666    for( i=begin; i<end; ++i )
667        tr_bitfieldAdd( bitfield, i );
668}
669
670void
671tr_bitfieldRem( tr_bitfield   * bitfield,
672                size_t          nth )
673{
674    static const uint8_t rems[8] = { 127, 191, 223, 239, 247, 251, 253, 254 };
675
676    if( bitfield != NULL )
677        bitfield->bits[nth>>3u] &= rems[nth&7u];
678
679    assert( !tr_bitfieldHas( bitfield, nth ) );
680}
681
682void
683tr_bitfieldRemRange ( tr_bitfield  * b,
684                      size_t         begin,
685                      size_t         end )
686{
687    /* TODO: there are faster ways to do this */
688    unsigned int i;
689    for( i=begin; i<end; ++i )
690        tr_bitfieldRem( b, i );
691}
692
693tr_bitfield*
694tr_bitfieldOr( tr_bitfield * a, const tr_bitfield * b )
695{
696    uint8_t *ait;
697    const uint8_t *aend, *bit;
698
699    assert( a->len == b->len );
700
701    for( ait=a->bits, bit=b->bits, aend=ait+a->len; ait!=aend; )
702        *ait++ |= *bit++;
703
704    return a;
705}
706
707size_t
708tr_bitfieldCountTrueBits( const tr_bitfield* b )
709{
710    size_t ret = 0;
711    const uint8_t *it, *end;
712    static const int trueBitCount[512] = {
713        0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,
714        1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
715        1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
716        2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
717        1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
718        2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
719        2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
720        3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8,
721        1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
722        2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
723        2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
724        3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8,
725        2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
726        3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8,
727        3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8,
728        4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8,5,6,6,7,6,7,7,8,6,7,7,8,7,8,8,9
729    };
730
731    if( !b )
732        return 0;
733
734    for( it=b->bits, end=it+b->len; it!=end; ++it )
735        ret += trueBitCount[*it];
736
737    return ret;
738}
739
740/***
741****
742***/
743
744uint64_t
745tr_date( void )
746{
747    struct timeval tv;
748    gettimeofday( &tv, NULL );
749    return (uint64_t) tv.tv_sec * 1000 + ( tv.tv_usec / 1000 );
750}
751
752void
753tr_wait( uint64_t delay_milliseconds )
754{
755#ifdef __BEOS__
756    snooze( 1000 * delay_milliseconds );
757#elif defined(WIN32)
758    Sleep( (DWORD)delay_milliseconds );
759#else
760    usleep( 1000 * delay_milliseconds );
761#endif
762}
763
764/***
765****
766***/
767
768
769#ifndef HAVE_STRLCPY
770
771/*
772 * Copy src to string dst of size siz.  At most siz-1 characters
773 * will be copied.  Always NUL terminates (unless siz == 0).
774 * Returns strlen(src); if retval >= siz, truncation occurred.
775 */
776size_t
777strlcpy(char *dst, const char *src, size_t siz)
778{
779        char *d = dst;
780        const char *s = src;
781        size_t n = siz;
782
783        /* Copy as many bytes as will fit */
784        if (n != 0) {
785                while (--n != 0) {
786                        if ((*d++ = *s++) == '\0')
787                                break;
788                }
789        }
790
791        /* Not enough room in dst, add NUL and traverse rest of src */
792        if (n == 0) {
793                if (siz != 0)
794                        *d = '\0';              /* NUL-terminate dst */
795                while (*s++)
796                        ;
797        }
798
799        return(s - src - 1);    /* count does not include NUL */
800}
801
802#endif /* HAVE_STRLCPY */
803
804
805#ifndef HAVE_STRLCAT
806
807/*
808 * Appends src to string dst of size siz (unlike strncat, siz is the
809 * full size of dst, not space left).  At most siz-1 characters
810 * will be copied.  Always NUL terminates (unless siz <= strlen(dst)).
811 * Returns strlen(src) + MIN(siz, strlen(initial dst)).
812 * If retval >= siz, truncation occurred.
813 */
814size_t
815strlcat(char *dst, const char *src, size_t siz)
816{
817        char *d = dst;
818        const char *s = src;
819        size_t n = siz;
820        size_t dlen;
821
822        /* Find the end of dst and adjust bytes left but don't go past end */
823        while (n-- != 0 && *d != '\0')
824                d++;
825        dlen = d - dst;
826        n = siz - dlen;
827
828        if (n == 0)
829                return(dlen + strlen(s));
830        while (*s != '\0') {
831                if (n != 1) {
832                        *d++ = *s;
833                        n--;
834                }
835                s++;
836        }
837        *d = '\0';
838
839        return(dlen + (s - src));       /* count does not include NUL */
840}
841
842#endif /* HAVE_STRLCAT */
Note: See TracBrowser for help on using the repository browser.