source: branches/daemon/libtransmission/metainfo.c @ 1634

Last change on this file since 1634 was 1634, checked in by joshe, 15 years ago

Add API function for adding a torrent from data, instead of a filename.

  • Property svn:keywords set to Date Rev Author Id
File size: 21.9 KB
Line 
1/******************************************************************************
2 * $Id: metainfo.c 1634 2007-04-02 20:32:20Z joshe $
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 "transmission.h"
26
27#define TORRENT_MAX_SIZE (5*1024*1024)
28
29/***********************************************************************
30 * Local prototypes
31 **********************************************************************/
32static int realparse( tr_info_t * inf, uint8_t * buf, size_t len );
33static void savedname( char * name, size_t len, const char * hash,
34                       const char * tag );
35static char * readtorrent( const char * path, size_t * len );
36static int savetorrent( const char * hash, const char * tag,
37                        const uint8_t * buf, size_t buflen );
38static int getfile( char * buf, int size,
39                    const char * prefix, benc_val_t * name );
40static int getannounce( tr_info_t * inf, benc_val_t * meta );
41static char * announceToScrape( const char * announce );
42static int parseFiles( tr_info_t * inf, benc_val_t * name,
43                       benc_val_t * files, benc_val_t * length );
44static void strcatUTF8( char *, int, const char *, int );
45
46/***********************************************************************
47 * tr_metainfoParse
48 ***********************************************************************
49 *
50 **********************************************************************/
51int
52tr_metainfoParseFile( tr_info_t * inf, const char * tag,
53                      const char * path, int save )
54{
55    uint8_t * buf;
56    size_t    size;
57
58    /* read the torrent data */
59    buf = readtorrent( path, &size );
60    if( NULL == buf )
61    {
62        return 1;
63    }
64
65    if( realparse( inf, buf, size ) )
66    {
67        free( buf );
68        return 1;
69    }
70
71    if( save )
72    {
73        if( savetorrent( inf->hashString, tag, buf, size ) )
74        {
75            free( buf );
76            return 1;
77        }
78        savedname( inf->torrent, sizeof inf->torrent, inf->hashString, tag );
79    }
80
81    free( buf );
82
83    return 0;
84}
85
86int
87tr_metainfoParseData( tr_info_t * inf, const char * tag,
88                      uint8_t * data, size_t size, int save )
89{
90    if( realparse( inf, data, size ) )
91    {
92        return 1;
93    }
94
95    if( save )
96    {
97        if( savetorrent( inf->hashString, tag, data, size ) )
98        {
99            return 1;
100        }
101        savedname( inf->torrent, sizeof inf->torrent, inf->hashString, tag );
102    }
103
104    return 0;
105}
106
107int
108tr_metainfoParseHash( tr_info_t * inf, const char * tag, const char * hash )
109{
110    struct stat sb;
111    uint8_t   * buf;
112    size_t      size;
113    int         save;
114
115    /* check it we should use an old file without a tag */
116    /* XXX this should go away at some point */
117    save = 0;
118    savedname( inf->torrent, sizeof inf->torrent, hash, tag );
119    if( 0 > stat( inf->torrent, &sb ) && ENOENT == errno )
120    {
121        savedname( inf->torrent, sizeof inf->torrent, hash, NULL );
122        if( 0 == stat( inf->torrent, &sb ))
123        {
124            save = 1;
125        }
126    }
127
128    buf = readtorrent( inf->torrent, &size );
129    if( NULL == buf )
130    {
131        return 1;
132    }
133
134    if( realparse( inf, buf, size ) )
135    {
136        free( buf );
137        return 1;
138    }
139
140    /* save a new tagged copy of the old untagged torrent */
141    if( save )
142    {
143        if( savetorrent( hash, tag, buf, size ) )
144        {
145            free( buf );
146            return 1;
147        }
148        savedname( inf->torrent, sizeof inf->torrent, hash, tag );
149    }
150
151    free( buf );
152
153    return 0;
154}
155
156int
157realparse( tr_info_t * inf, uint8_t * buf, size_t size )
158{
159    benc_val_t   meta, * beInfo, * val, * val2;
160    int          i;
161
162    /* Parse bencoded infos */
163    if( tr_bencLoad( buf, size, &meta, NULL ) )
164    {
165        tr_err( "Error while parsing bencoded data" );
166        return 1;
167    }
168
169    /* Get info hash */
170    beInfo = tr_bencDictFind( &meta, "info" );
171    if( NULL == beInfo || TYPE_DICT != beInfo->type )
172    {
173        tr_err( "%s \"info\" dictionary", ( beInfo ? "Invalid" : "Missing" ) );
174        tr_bencFree( &meta );
175        return 1;
176    }
177    SHA1( (uint8_t *) beInfo->begin,
178          (long) beInfo->end - (long) beInfo->begin, inf->hash );
179    for( i = 0; i < SHA_DIGEST_LENGTH; i++ )
180    {
181        snprintf( inf->hashString + i * 2, sizeof( inf->hashString ) - i * 2,
182                  "%02x", inf->hash[i] );
183    }
184
185    /* Comment info */
186    val = tr_bencDictFindFirst( &meta, "comment.utf-8", "comment", NULL );
187    if( NULL != val && TYPE_STR == val->type )
188    {
189        strcatUTF8( inf->comment, sizeof( inf->comment ), val->val.s.s, 0 );
190    }
191   
192    /* Creator info */
193    val = tr_bencDictFindFirst( &meta, "created by.utf-8", "created by", NULL );
194    if( NULL != val && TYPE_STR == val->type )
195    {
196        strcatUTF8( inf->creator, sizeof( inf->creator ), val->val.s.s, 0 );
197    }
198   
199    /* Date created */
200    inf->dateCreated = 0;
201    val = tr_bencDictFind( &meta, "creation date" );
202    if( NULL != val && TYPE_INT == val->type )
203    {
204        inf->dateCreated = val->val.i;
205    }
206   
207    /* Private torrent */
208    val  = tr_bencDictFind( beInfo, "private" );
209    val2 = tr_bencDictFind( &meta,  "private" );
210    if( ( NULL != val  && ( TYPE_INT != val->type  || 0 != val->val.i ) ) ||
211        ( NULL != val2 && ( TYPE_INT != val2->type || 0 != val2->val.i ) ) )
212    {
213        inf->flags |= TR_FLAG_PRIVATE;
214    }
215   
216    /* Piece length */
217    val = tr_bencDictFind( beInfo, "piece length" );
218    if( NULL == val || TYPE_INT != val->type )
219    {
220        tr_err( "%s \"piece length\" entry", ( val ? "Invalid" : "Missing" ) );
221        goto fail;
222    }
223    inf->pieceSize = val->val.i;
224
225    /* Hashes */
226    val = tr_bencDictFind( beInfo, "pieces" );
227    if( NULL == val || TYPE_STR != val->type )
228    {
229        tr_err( "%s \"pieces\" entry", ( val ? "Invalid" : "Missing" ) );
230        goto fail;
231    }
232    if( val->val.s.i % SHA_DIGEST_LENGTH )
233    {
234        tr_err( "Invalid \"piece\" string (size is %d)", val->val.s.i );
235        goto fail;
236    }
237    inf->pieceCount = val->val.s.i / SHA_DIGEST_LENGTH;
238    inf->pieces = (uint8_t *) tr_bencStealStr( val );
239
240    /* TODO add more tests so we don't crash on weird files */
241
242    /* get file or top directory name */
243    val = tr_bencDictFindFirst( beInfo, "name.utf-8", "name", NULL );
244    if( parseFiles( inf, tr_bencDictFindFirst( beInfo,
245                                               "name.utf-8", "name", NULL ),
246                    tr_bencDictFind( beInfo, "files" ),
247                    tr_bencDictFind( beInfo, "length" ) ) )
248    {
249        goto fail;
250    }
251
252    if( (uint64_t) inf->pieceCount !=
253        ( inf->totalSize + inf->pieceSize - 1 ) / inf->pieceSize )
254    {
255        tr_err( "Size of hashes and files don't match" );
256        goto fail;
257    }
258
259    /* get announce or announce-list */
260    if( getannounce( inf, &meta ) )
261    {
262        goto fail;
263    }
264
265    tr_bencFree( &meta );
266    return 0;
267
268  fail:
269    tr_metainfoFree( inf );
270    tr_bencFree( &meta );
271    return 1;
272}
273
274void tr_metainfoFree( tr_info_t * inf )
275{
276    int ii, jj;
277
278    free( inf->pieces );
279    free( inf->files );
280   
281    for( ii = 0; ii < inf->trackerTiers; ii++ )
282    {
283        for( jj = 0; jj < inf->trackerList[ii].count; jj++ )
284        {
285            free( inf->trackerList[ii].list[jj].address );
286            free( inf->trackerList[ii].list[jj].announce );
287            free( inf->trackerList[ii].list[jj].scrape );
288        }
289        free( inf->trackerList[ii].list );
290    }
291    free( inf->trackerList );
292}
293
294static int getfile( char * buf, int size,
295                    const char * prefix, benc_val_t * name )
296{
297    benc_val_t  * dir;
298    const char ** list;
299    int           ii, jj;
300
301    if( TYPE_LIST != name->type )
302    {
303        return 1;
304    }
305
306    list = calloc( name->val.l.count, sizeof( list[0] ) );
307    if( NULL == list )
308    {
309        return 1;
310    }
311
312    for( ii = jj = 0; name->val.l.count > ii; ii++ )
313    {
314        dir = &name->val.l.vals[ii];
315        if( TYPE_STR != dir->type )
316        {
317            continue;
318        }
319        if( 0 == strcmp( "..", dir->val.s.s ) )
320        {
321            if( 0 < jj )
322            {
323                jj--;
324            }
325        }
326        else if( 0 != strcmp( ".", dir->val.s.s ) )
327        {
328            list[jj] = dir->val.s.s;
329            jj++;
330        }
331    }
332
333    if( 0 == jj )
334    {
335        return 1;
336    }
337
338    strcatUTF8( buf, size, prefix, 0 );
339    for( ii = 0; jj > ii; ii++ )
340    {
341        strcatUTF8( buf, size, "/", 0 );
342        strcatUTF8( buf, size, list[ii], 1 );
343    }
344    free( list );
345
346    return 0;
347}
348
349static int getannounce( tr_info_t * inf, benc_val_t * meta )
350{
351    benc_val_t        * val, * subval, * urlval;
352    char              * address, * announce;
353    int                 ii, jj, port, random, subcount;
354    tr_tracker_info_t * sublist;
355    void * swapping;
356
357    /* Announce-list */
358    val = tr_bencDictFind( meta, "announce-list" );
359    if( NULL != val && TYPE_LIST == val->type && 0 < val->val.l.count )
360    {
361        inf->trackerTiers = 0;
362        inf->trackerList = calloc( sizeof( inf->trackerList[0] ),
363                                   val->val.l.count );
364
365        /* iterate through the announce-list's tiers */
366        for( ii = 0; ii < val->val.l.count; ii++ )
367        {
368            subval = &val->val.l.vals[ii];
369            if( TYPE_LIST != subval->type || 0 >= subval->val.l.count )
370            {
371                continue;
372            }
373            subcount = 0;
374            sublist = calloc( sizeof( sublist[0] ), subval->val.l.count );
375
376            /* iterate through the tier's items */
377            for( jj = 0; jj < subval->val.l.count; jj++ )
378            {
379                urlval = &subval->val.l.vals[jj];
380                if( TYPE_STR != urlval->type ||
381                    tr_httpParseUrl( urlval->val.s.s, urlval->val.s.i,
382                                     &address, &port, &announce ) )
383                {
384                    continue;
385                }
386
387                /* place the item info in a random location in the sublist */
388                random = tr_rand( subcount + 1 );
389                if( random != subcount )
390                {
391                    sublist[subcount] = sublist[random];
392                }
393                sublist[random].address  = address;
394                sublist[random].port     = port;
395                sublist[random].announce = announce;
396                sublist[random].scrape   = announceToScrape( announce );
397                subcount++;
398            }
399
400            /* just use sublist as-is if it's full */
401            if( subcount == subval->val.l.count )
402            {
403                inf->trackerList[inf->trackerTiers].list = sublist;
404                inf->trackerList[inf->trackerTiers].count = subcount;
405                inf->trackerTiers++;
406            }
407            /* if we skipped some of the tier's items then trim the sublist */
408            else if( 0 < subcount )
409            {
410                inf->trackerList[inf->trackerTiers].list = calloc( sizeof( sublist[0] ), subcount );
411                memcpy( inf->trackerList[inf->trackerTiers].list, sublist,
412                        sizeof( sublist[0] ) * subcount );
413                inf->trackerList[inf->trackerTiers].count = subcount;
414                inf->trackerTiers++;
415                free( sublist );
416            }
417            /* drop the whole sublist if we didn't use any items at all */
418            else
419            {
420                free( sublist );
421            }
422        }
423
424        /* did we use any of the tiers? */
425        if( 0 == inf->trackerTiers )
426        {
427            tr_inf( "No valid entries in \"announce-list\"" );
428            free( inf->trackerList );
429            inf->trackerList = NULL;
430        }
431        /* trim unused sublist pointers */
432        else if( inf->trackerTiers < val->val.l.count )
433        {
434            swapping = inf->trackerList;
435            inf->trackerList = calloc( sizeof( inf->trackerList[0] ),
436                                       inf->trackerTiers );
437            memcpy( inf->trackerList, swapping,
438                    sizeof( inf->trackerList[0] ) * inf->trackerTiers );
439            free( swapping );
440        }
441    }
442
443    /* Regular announce value */
444    if( 0 == inf->trackerTiers )
445    {
446        val = tr_bencDictFind( meta, "announce" );
447        if( NULL == val || TYPE_STR != val->type )
448        {
449            tr_err( "No \"announce\" entry" );
450            return 1;
451        }
452
453        if( tr_httpParseUrl( val->val.s.s, val->val.s.i,
454                             &address, &port, &announce ) )
455        {
456            tr_err( "Invalid announce URL (%s)", val->val.s.s );
457            return 1;
458        }
459
460        sublist                   = calloc( sizeof( sublist[0] ), 1 );
461        sublist[0].address        = address;
462        sublist[0].port           = port;
463        sublist[0].announce       = announce;
464        sublist[0].scrape         = announceToScrape( announce );
465        inf->trackerList          = calloc( sizeof( inf->trackerList[0] ), 1 );
466        inf->trackerList[0].list  = sublist;
467        inf->trackerList[0].count = 1;
468        inf->trackerTiers         = 1;
469    }
470
471    return 0;
472}
473
474static char * announceToScrape( const char * announce )
475{   
476    char old[]  = "announce";
477    int  oldlen = 8;
478    char new[]  = "scrape";
479    int  newlen = 6;
480    char * slash, * scrape;
481    size_t scrapelen, used;
482
483    slash = strrchr( announce, '/' );
484    if( NULL == slash )
485    {
486        return NULL;
487    }
488    slash++;
489   
490    if( 0 != strncmp( slash, old, oldlen ) )
491    {
492        return NULL;
493    }
494
495    scrapelen = strlen( announce ) - oldlen + newlen;
496    scrape = calloc( scrapelen + 1, 1 );
497    if( NULL == scrape )
498    {
499        return NULL;
500    }
501    assert( ( size_t )( slash - announce ) < scrapelen );
502    memcpy( scrape, announce, slash - announce );
503    used = slash - announce;
504    strncat( scrape, new, scrapelen - used );
505    used += newlen;
506    assert( strlen( scrape ) == used );
507    if( used < scrapelen )
508    {
509        assert( strlen( slash + oldlen ) == scrapelen - used );
510        strncat( scrape, slash + oldlen, scrapelen - used );
511    }
512
513    return scrape;
514}
515
516void savedname( char * name, size_t len, const char * hash, const char * tag )
517{
518    if( NULL == tag )
519    {
520        snprintf( name, len, "%s/%s", tr_getTorrentsDirectory(), hash );
521    }
522    else
523    {
524        snprintf( name, len, "%s/%s-%s",
525                  tr_getTorrentsDirectory(), hash, tag );
526    }
527}
528
529void tr_metainfoRemoveSaved( const char * hashString, const char * tag )
530{
531    char file[MAX_PATH_LENGTH];
532
533    savedname( file, sizeof file, hashString, tag );
534    unlink(file);
535}
536
537char * readtorrent( const char * path, size_t * size )
538{
539    char       * buf;
540    struct stat  sb;
541    FILE       * file;
542
543    /* try to stat the file */
544    if( stat( path, &sb ) )
545    {
546        tr_err( "Could not stat file (%s)", path );
547        return NULL;
548    }
549
550    if( ( sb.st_mode & S_IFMT ) != S_IFREG )
551    {
552        tr_err( "Not a regular file (%s)", path );
553        return NULL;
554    }
555    if( sb.st_size > TORRENT_MAX_SIZE )
556    {
557        tr_err( "Torrent file is too big (%"PRIu64" bytes)",
558                ( uint64_t )sb.st_size );
559        return NULL;
560    }
561
562    /* Load the torrent file into our buffer */
563    file = fopen( path, "rb" );
564    if( !file )
565    {
566        tr_err( "Could not open file (%s)", path );
567        return NULL;
568    }
569    buf = malloc( sb.st_size );
570    if( NULL == buf )
571    {
572        tr_err( "Could not allocate memory (%"PRIu64" bytes)",
573                ( uint64_t )sb.st_size );
574    }
575    fseek( file, 0, SEEK_SET );
576    if( fread( buf, sb.st_size, 1, file ) != 1 )
577    {
578        tr_err( "Read error (%s)", path );
579        free( buf );
580        fclose( file );
581        return NULL;
582    }
583    fclose( file );
584
585    *size = sb.st_size;
586
587    return buf;
588}
589
590/* Save a copy of the torrent file in the saved torrent directory */
591int savetorrent( const char * hash, const char * tag,
592                 const uint8_t * buf, size_t buflen )
593{
594    char   path[MAX_PATH_LENGTH];
595    FILE * file;
596
597    savedname( path, sizeof path, hash, tag );
598    file = fopen( path, "wb" );
599    if( !file )
600    {
601        tr_err( "Could not open file (%s) (%s)", path, strerror( errno ) );
602        return 1;
603    }
604    fseek( file, 0, SEEK_SET );
605    if( fwrite( buf, 1, buflen, file ) != buflen )
606    {
607        tr_err( "Could not write file (%s) (%s)", path, strerror( errno ) );
608        fclose( file );
609        return 1;
610    }
611    fclose( file );
612
613    return 0;
614}
615
616int
617parseFiles( tr_info_t * inf, benc_val_t * name,
618            benc_val_t * files, benc_val_t * length )
619{
620    benc_val_t * item, * path;
621    int ii;
622
623    if( NULL == name || TYPE_STR != name->type )
624    {
625        tr_err( "%s \"name\" string", ( name ? "Invalid" : "Missing" ) );
626        return 1;
627    }
628
629    strcatUTF8( inf->name, sizeof( inf->name ), name->val.s.s, 1 );
630    if( '\0' == inf->name[0] )
631    {
632        tr_err( "Invalid \"name\" string" );
633        return 1;
634    }
635    inf->totalSize = 0;
636
637    if( files && TYPE_LIST == files->type )
638    {
639        /* Multi-file mode */
640        inf->multifile = 1;
641        inf->fileCount = files->val.l.count;
642        inf->files     = calloc( inf->fileCount, sizeof( inf->files[0] ) );
643
644        if( NULL == inf->files )
645        {
646            return 1;
647        }
648
649        for( ii = 0; files->val.l.count > ii; ii++ )
650        {
651            item = &files->val.l.vals[ii];
652            path = tr_bencDictFindFirst( item, "path.utf-8", "path", NULL );
653            if( getfile( inf->files[ii].name, sizeof( inf->files[0].name ),
654                         inf->name, path ) )
655            {
656                tr_err( "%s \"path\" entry",
657                        ( path ? "Invalid" : "Missing" ) );
658                return 1;
659            }
660            length = tr_bencDictFind( item, "length" );
661            if( NULL == length || TYPE_INT != length->type )
662            {
663                tr_err( "%s \"length\" entry",
664                        ( length ? "Invalid" : "Missing" ) );
665                return 1;
666            }
667            inf->files[ii].length = length->val.i;
668            inf->totalSize         += length->val.i;
669        }
670    }
671    else if( NULL != length && TYPE_INT == length->type )
672    {
673        /* Single-file mode */
674        inf->multifile = 0;
675        inf->fileCount = 1;
676        inf->files     = calloc( 1, sizeof( inf->files[0] ) );
677
678        if( NULL == inf->files )
679        {
680            return 1;
681        }
682
683        strcatUTF8( inf->files[0].name, sizeof( inf->files[0].name ),
684                    name->val.s.s, 1 );
685
686        inf->files[0].length = length->val.i;
687        inf->totalSize      += length->val.i;
688    }
689    else
690    {
691        tr_err( "%s \"files\" entry and %s \"length\" entry",
692                ( files ? "Invalid" : "Missing" ),
693                ( length ? "invalid" : "missing" ) );
694    }
695
696    return 0;
697}
698
699/***********************************************************************
700 * strcatUTF8
701 ***********************************************************************
702 * According to the official specification, all strings in the torrent
703 * file are supposed to be UTF-8 encoded. However, there are
704 * non-compliant torrents around... If we encounter an invalid UTF-8
705 * character, we assume it is ISO 8859-1 and convert it to UTF-8.
706 **********************************************************************/
707#define WANTBYTES( want, got ) \
708    if( (want) > (got) ) { return; } else { (got) -= (want); }
709static void strcatUTF8( char * s, int len, const char * append, int deslash )
710{
711    const char * p;
712
713    /* don't overwrite the nul at the end */
714    len--;
715
716    /* Go to the end of the destination string */
717    while( s[0] )
718    {
719        s++;
720        len--;
721    }
722
723    /* Now start appending, converting on the fly if necessary */
724    for( p = append; p[0]; )
725    {
726        /* skip over / if requested */
727        if( deslash && '/' == p[0] )
728        {
729            p++;
730            continue;
731        }
732
733        if( !( p[0] & 0x80 ) )
734        {
735            /* ASCII character */
736            WANTBYTES( 1, len );
737            *(s++) = *(p++);
738            continue;
739        }
740
741        if( ( p[0] & 0xE0 ) == 0xC0 && ( p[1] & 0xC0 ) == 0x80 )
742        {
743            /* 2-bytes UTF-8 character */
744            WANTBYTES( 2, len );
745            *(s++) = *(p++); *(s++) = *(p++);
746            continue;
747        }
748
749        if( ( p[0] & 0xF0 ) == 0xE0 && ( p[1] & 0xC0 ) == 0x80 &&
750            ( p[2] & 0xC0 ) == 0x80 )
751        {
752            /* 3-bytes UTF-8 character */
753            WANTBYTES( 3, len );
754            *(s++) = *(p++); *(s++) = *(p++);
755            *(s++) = *(p++);
756            continue;
757        }
758
759        if( ( p[0] & 0xF8 ) == 0xF0 && ( p[1] & 0xC0 ) == 0x80 &&
760            ( p[2] & 0xC0 ) == 0x80 && ( p[3] & 0xC0 ) == 0x80 )
761        {
762            /* 4-bytes UTF-8 character */
763            WANTBYTES( 4, len );
764            *(s++) = *(p++); *(s++) = *(p++);
765            *(s++) = *(p++); *(s++) = *(p++);
766            continue;
767        }
768
769        /* ISO 8859-1 -> UTF-8 conversion */
770        WANTBYTES( 2, len );
771        *(s++) = 0xC0 | ( ( *p & 0xFF ) >> 6 );
772        *(s++) = 0x80 | ( *(p++) & 0x3F );
773    }
774}
Note: See TracBrowser for help on using the repository browser.