source: trunk/libtransmission/metainfo.c @ 10774

Last change on this file since 10774 was 10774, checked in by charles, 12 years ago

(libT) #3291 "tr_torrent.infoDictOffset should be lazily evaluated"

  • Property svn:keywords set to Date Rev Author Id
File size: 17.4 KB
Line 
1/*
2 * This file Copyright (C) 2009-2010 Mnemosyne LLC
3            trackers[trackerCount].id = 0;
4 *
5 * This file is licensed by the GPL version 2.  Works owned by the
6 * Transmission project are granted a special exemption to clause 2(b)
7 * so that the bulk of its code can remain under the MIT license.
8 * This exemption does not extend to derived works not owned by
9 * the Transmission project.
10 *
11 * $Id: metainfo.c 10774 2010-06-16 03:05:23Z charles $
12 */
13
14#include <assert.h>
15#include <errno.h>
16#include <stdio.h>
17#include <string.h>
18
19#include <sys/types.h>
20#include <sys/stat.h>
21#include <unistd.h> /* unlink, stat */
22
23#include <event.h> /* struct evbuffer */
24
25#include "transmission.h"
26#include "session.h"
27#include "bencode.h"
28#include "crypto.h" /* tr_sha1 */
29#include "metainfo.h"
30#include "platform.h"
31#include "utils.h"
32
33/***
34****
35***/
36
37char*
38tr_metainfoGetBasename( const tr_info * inf )
39{
40    char *ret, *pch, *name;
41
42    name = tr_strdup( inf->name );
43    for( pch=name; pch && *pch; ++pch )
44        if( *pch == '/' )
45            *pch = '_';
46   
47    ret = tr_strdup_printf( "%s.%16.16s", name, inf->hashString );
48
49    tr_free( name );
50    return ret;
51}
52
53static char*
54getTorrentFilename( const tr_session * session,
55                    const tr_info *   inf )
56{
57    char * base = tr_metainfoGetBasename( inf );
58    char * filename = tr_strdup_printf( "%s" TR_PATH_DELIMITER_STR "%s.torrent",
59                                        tr_getTorrentDir( session ), base );
60    tr_free( base );
61    return filename;
62}
63
64static char*
65getOldTorrentFilename( const tr_session * session, const tr_info * inf )
66{
67    int i;
68    char * path;
69    struct stat sb;
70    const int tagCount = 5;
71    const char * tags[] = { "beos", "cli", "daemon", "macosx", "wx" };
72
73    /* test the beos, cli, daemon, macosx, wx tags */
74    for( i=0; i<tagCount; ++i ) {
75        path = tr_strdup_printf( "%s%c%s-%s", tr_getTorrentDir( session ), '/', inf->hashString, tags[i] );
76        if( !stat( path, &sb ) && ( ( sb.st_mode & S_IFMT ) == S_IFREG ) )
77            return path;
78        tr_free( path );
79    }
80
81    /* test a non-tagged file */
82    path = tr_buildPath( tr_getTorrentDir( session ), inf->hashString, NULL );
83    if( !stat( path, &sb ) && ( ( sb.st_mode & S_IFMT ) == S_IFREG ) )
84        return path;
85    tr_free( path );
86
87    /* return the -gtk form by default, since that's the most common case.
88       don't bother testing stat() on it since this is the last candidate
89       and we don't want to return NULL anyway */
90    return tr_strdup_printf( "%s%c%s-%s", tr_getTorrentDir( session ), '/', inf->hashString, "gtk" );
91}
92
93/* this is for really old versions of T and will probably be removed someday */
94void
95tr_metainfoMigrate( tr_session * session,
96                    tr_info *   inf )
97{
98    struct stat new_sb;
99    char *      name = getTorrentFilename( session, inf );
100
101    if( stat( name, &new_sb ) || ( ( new_sb.st_mode & S_IFMT ) != S_IFREG ) )
102    {
103        char *    old_name = getOldTorrentFilename( session, inf );
104        size_t    contentLen;
105        uint8_t * content;
106
107        tr_mkdirp( tr_getTorrentDir( session ), 0777 );
108        if( ( content = tr_loadFile( old_name, &contentLen ) ) )
109        {
110            FILE * out;
111            errno = 0;
112            out = fopen( name, "wb+" );
113            if( !out )
114            {
115                tr_nerr( inf->name, _( "Couldn't create \"%1$s\": %2$s" ),
116                        name, tr_strerror( errno ) );
117            }
118            else
119            {
120                if( fwrite( content, sizeof( uint8_t ), contentLen, out )
121                    == contentLen )
122                {
123                    tr_free( inf->torrent );
124                    inf->torrent = tr_strdup( name );
125                    tr_sessionSetTorrentFile( session, inf->hashString, name );
126                    unlink( old_name );
127                }
128                fclose( out );
129            }
130        }
131
132        tr_free( content );
133        tr_free( old_name );
134    }
135
136    tr_free( name );
137}
138
139/***
140****
141***/
142
143static tr_bool
144path_is_suspicious( const char * path )
145{
146    return ( path == NULL )
147        || ( strstr( path, "../" ) != NULL );
148}
149
150static tr_bool
151getfile( char ** setme, const char * root, tr_benc * path )
152{
153    tr_bool success = FALSE;
154
155    if( tr_bencIsList( path ) )
156    {
157        int i;
158        const int n = tr_bencListSize( path );
159        struct evbuffer * buf = evbuffer_new( );
160
161        evbuffer_add( buf, root, strlen( root ) );
162        for( i = 0; i < n; ++i )
163        {
164            const char * str;
165            if( tr_bencGetStr( tr_bencListChild( path, i ), &str ) )
166            {
167                evbuffer_add( buf, TR_PATH_DELIMITER_STR, 1 );
168                evbuffer_add( buf, str, strlen( str ) );
169            }
170        }
171
172        *setme = tr_utf8clean( (char*)EVBUFFER_DATA( buf ), EVBUFFER_LENGTH( buf ), NULL );
173        /* fprintf( stderr, "[%s]\n", *setme ); */
174        evbuffer_free( buf );
175        success = TRUE;
176    }
177
178    if( ( *setme != NULL ) && path_is_suspicious( *setme ) )
179    {
180        tr_free( *setme );
181        *setme = NULL;
182        success = FALSE;
183    }
184
185    return success;
186}
187
188static const char*
189parseFiles( tr_info * inf, tr_benc * files, const tr_benc * length )
190{
191    int64_t len;
192
193    inf->totalSize = 0;
194
195    if( tr_bencIsList( files ) ) /* multi-file mode */
196    {
197        tr_file_index_t i;
198
199        inf->isMultifile = 1;
200        inf->fileCount   = tr_bencListSize( files );
201        inf->files       = tr_new0( tr_file, inf->fileCount );
202
203        for( i = 0; i < inf->fileCount; ++i )
204        {
205            tr_benc * file;
206            tr_benc * path;
207
208            file = tr_bencListChild( files, i );
209            if( !tr_bencIsDict( file ) )
210                return "files";
211
212            if( !tr_bencDictFindList( file, "path.utf-8", &path ) )
213                if( !tr_bencDictFindList( file, "path", &path ) )
214                    return "path";
215
216            if( !getfile( &inf->files[i].name, inf->name, path ) )
217                return "path";
218
219            if( !tr_bencDictFindInt( file, "length", &len ) )
220                return "length";
221
222            inf->files[i].length = len;
223            inf->totalSize      += len;
224        }
225    }
226    else if( tr_bencGetInt( length, &len ) ) /* single-file mode */
227    {
228        if( path_is_suspicious( inf->name ) )
229            return "path";
230
231        inf->isMultifile      = 0;
232        inf->fileCount        = 1;
233        inf->files            = tr_new0( tr_file, 1 );
234        inf->files[0].name    = tr_strdup( inf->name );
235        inf->files[0].length  = len;
236        inf->totalSize       += len;
237    }
238    else
239    {
240        return "length";
241    }
242
243    return NULL;
244}
245
246static char *
247tr_convertAnnounceToScrape( const char * announce )
248{
249    char *       scrape = NULL;
250    const char * s;
251
252    /* To derive the scrape URL use the following steps:
253     * Begin with the announce URL. Find the last '/' in it.
254     * If the text immediately following that '/' isn't 'announce'
255     * it will be taken as a sign that that tracker doesn't support
256     * the scrape convention. If it does, substitute 'scrape' for
257     * 'announce' to find the scrape page.  */
258    if( ( ( s = strrchr( announce, '/' ) ) ) && !strncmp( ++s, "announce", 8 ) )
259    {
260        const char * prefix = announce;
261        const size_t prefix_len = s - announce;
262        const char * suffix = s + 8;
263        const size_t suffix_len = strlen( suffix );
264        const size_t alloc_len = prefix_len + 6 + suffix_len + 1;
265        char * walk = scrape = tr_new( char, alloc_len );
266        memcpy( walk, prefix, prefix_len ); walk += prefix_len;
267        memcpy( walk, "scrape", 6 );        walk += 6;
268        memcpy( walk, suffix, suffix_len ); walk += suffix_len;
269        *walk++ = '\0';
270        assert( walk - scrape == (int)alloc_len );
271    }
272
273    return scrape;
274}
275
276static const char*
277getannounce( tr_info * inf, tr_benc * meta )
278{
279    const char *      str;
280    tr_tracker_info * trackers = NULL;
281    int               trackerCount = 0;
282    tr_benc *         tiers;
283
284    /* Announce-list */
285    if( tr_bencDictFindList( meta, "announce-list", &tiers ) )
286    {
287        int       n;
288        int       i, j, validTiers;
289        const int numTiers = tr_bencListSize( tiers );
290
291        n = 0;
292        for( i = 0; i < numTiers; ++i )
293            n += tr_bencListSize( tr_bencListChild( tiers, i ) );
294
295        trackers = tr_new0( tr_tracker_info, n );
296
297        for( i = 0, validTiers = 0; i < numTiers; ++i )
298        {
299            tr_benc * tier = tr_bencListChild( tiers, i );
300            const int tierSize = tr_bencListSize( tier );
301            tr_bool anyAdded = FALSE;
302            for( j = 0; j < tierSize; ++j )
303            {
304                if( tr_bencGetStr( tr_bencListChild( tier, j ), &str ) )
305                {
306                    char * url = tr_strstrip( tr_strdup( str ) );
307                    if( tr_urlIsValidTracker( url ) )
308                    {
309                        tr_tracker_info * t = trackers + trackerCount;
310                        t->tier = validTiers;
311                        t->announce = tr_strdup( url );
312                        t->scrape = tr_convertAnnounceToScrape( url );
313                        t->id = trackerCount;
314
315                        anyAdded = TRUE;
316                        ++trackerCount;
317                    }
318                    tr_free( url );
319                }
320            }
321
322            if( anyAdded )
323                ++validTiers;
324        }
325
326        /* did we use any of the tiers? */
327        if( !trackerCount )
328        {
329            tr_free( trackers );
330            trackers = NULL;
331        }
332    }
333
334    /* Regular announce value */
335    if( !trackerCount
336      && tr_bencDictFindStr( meta, "announce", &str ) )
337    {
338        char * url = tr_strstrip( tr_strdup( str ) );
339        if( tr_urlIsValidTracker( url ) )
340        {
341            trackers = tr_new0( tr_tracker_info, 1 );
342            trackers[trackerCount].tier = 0;
343            trackers[trackerCount].announce = tr_strdup( url );
344            trackers[trackerCount].scrape = tr_convertAnnounceToScrape( url );
345            trackers[trackerCount].id = 0;
346            trackerCount++;
347            /*fprintf( stderr, "single announce: [%s]\n", url );*/
348        }
349        tr_free( url );
350    }
351
352    inf->trackers = trackers;
353    inf->trackerCount = trackerCount;
354
355    return NULL;
356}
357
358static void
359geturllist( tr_info * inf,
360            tr_benc * meta )
361{
362    tr_benc * urls;
363
364    if( tr_bencDictFindList( meta, "url-list", &urls ) )
365    {
366        int          i;
367        const char * url;
368        const int    n = tr_bencListSize( urls );
369
370        inf->webseedCount = 0;
371        inf->webseeds = tr_new0( char*, n );
372
373        for( i = 0; i < n; ++i )
374            if( tr_bencGetStr( tr_bencListChild( urls, i ), &url ) )
375                inf->webseeds[inf->webseedCount++] = tr_strdup( url );
376    }
377}
378
379static int
380is_rfc2396_alnum( char ch )
381{
382    return ( '0' <= ch && ch <= '9' )
383        || ( 'A' <= ch && ch <= 'Z' )
384        || ( 'a' <= ch && ch <= 'z' );
385}
386
387static void
388escape( char * out, const uint8_t * in, size_t in_len ) /* rfc2396 */
389{
390    const uint8_t *end = in + in_len;
391
392    while( in != end )
393        if( is_rfc2396_alnum( *in ) )
394            *out++ = (char) *in++;
395        else
396            out += tr_snprintf( out, 4, "%%%02X", (unsigned int)*in++ );
397
398    *out = '\0';
399}
400
401static const char*
402tr_metainfoParseImpl( const tr_session  * session,
403                      tr_info           * inf,
404                      tr_bool           * hasInfoDict,
405                      int               * infoDictLength,
406                      const tr_benc     * meta_in )
407{
408    int64_t         i;
409    size_t          raw_len;
410    const char *    str;
411    const uint8_t * raw;
412    tr_benc *       d;
413    tr_benc *       infoDict = NULL;
414    tr_benc *       meta = (tr_benc *) meta_in;
415    tr_bool         err;
416    tr_bool         b;
417    tr_bool         isMagnet = FALSE;
418
419    /* info_hash: urlencoded 20-byte SHA1 hash of the value of the info key
420     * from the Metainfo file. Note that the value will be a bencoded
421     * dictionary, given the definition of the info key above. */
422    b = tr_bencDictFindDict( meta, "info", &infoDict );
423    if( hasInfoDict != NULL )
424        *hasInfoDict = b;
425    if( !b )
426    {
427        /* no info dictionary... is this a magnet link? */
428        if( tr_bencDictFindDict( meta, "magnet-info", &d ) )
429        {
430            isMagnet = TRUE;
431
432            /* get the info-hash */
433            if( !tr_bencDictFindRaw( d, "info_hash", &raw, &raw_len ) )
434                return "info_hash";
435            if( raw_len != SHA_DIGEST_LENGTH )
436                return "info_hash";
437            memcpy( inf->hash, raw, raw_len );
438            tr_sha1_to_hex( inf->hashString, inf->hash );
439            escape( inf->hashEscaped, inf->hash, SHA_DIGEST_LENGTH );
440
441            /* maybe get the display name */
442            if( tr_bencDictFindStr( d, "display-name", &str ) ) {
443                tr_free( inf->name );
444                inf->name = tr_strdup( str );
445            }
446        }
447        else /* not a magnet link and has no info dict... */
448        {
449            return "info";
450        }
451    }
452    else
453    {
454        int len;
455        char * bstr = tr_bencToStr( infoDict, TR_FMT_BENC, &len );
456        tr_sha1( inf->hash, bstr, len, NULL );
457        tr_sha1_to_hex( inf->hashString, inf->hash );
458        escape( inf->hashEscaped, inf->hash, SHA_DIGEST_LENGTH );
459
460        if( infoDictLength != NULL )
461            *infoDictLength = len;
462
463        tr_free( bstr );
464    }
465
466    /* name */
467    if( !isMagnet ) {
468        if( !tr_bencDictFindStr( infoDict, "name.utf-8", &str ) )
469            if( !tr_bencDictFindStr( infoDict, "name", &str ) )
470                str = "";
471        if( !str || !*str )
472            return "name";
473        tr_free( inf->name );
474        inf->name = tr_utf8clean( str, -1, &err );
475    }
476
477    /* comment */
478    if( !tr_bencDictFindStr( meta, "comment.utf-8", &str ) )
479        if( !tr_bencDictFindStr( meta, "comment", &str ) )
480            str = "";
481    tr_free( inf->comment );
482    inf->comment = tr_utf8clean( str, -1, &err );
483
484    /* created by */
485    if( !tr_bencDictFindStr( meta, "created by.utf-8", &str ) )
486        if( !tr_bencDictFindStr( meta, "created by", &str ) )
487            str = "";
488    tr_free( inf->creator );
489    inf->creator = tr_utf8clean( str, -1, &err );
490
491    /* creation date */
492    if( !tr_bencDictFindInt( meta, "creation date", &i ) )
493        i = 0;
494    inf->dateCreated = i;
495
496    /* private */
497    if( !tr_bencDictFindInt( infoDict, "private", &i ) )
498        if( !tr_bencDictFindInt( meta, "private", &i ) )
499            i = 0;
500    inf->isPrivate = i != 0;
501
502    /* piece length */
503    if( !isMagnet ) {
504        if( !tr_bencDictFindInt( infoDict, "piece length", &i ) || ( i < 1 ) )
505            return "piece length";
506        inf->pieceSize = i;
507    }
508
509    /* pieces */
510    if( !isMagnet ) {
511        if( !tr_bencDictFindRaw( infoDict, "pieces", &raw, &raw_len ) )
512            return "pieces";
513        if( raw_len % SHA_DIGEST_LENGTH )
514            return "pieces";
515        inf->pieceCount = raw_len / SHA_DIGEST_LENGTH;
516        inf->pieces = tr_new0( tr_piece, inf->pieceCount );
517        for( i = 0; i < inf->pieceCount; ++i )
518            memcpy( inf->pieces[i].hash, &raw[i * SHA_DIGEST_LENGTH],
519                    SHA_DIGEST_LENGTH );
520    }
521
522    /* files */
523    if( !isMagnet ) {
524        if( ( str = parseFiles( inf, tr_bencDictFind( infoDict, "files" ),
525                                     tr_bencDictFind( infoDict, "length" ) ) ) )
526            return str;
527        if( !inf->fileCount || !inf->totalSize )
528            return "files";
529        if( (uint64_t) inf->pieceCount !=
530           ( inf->totalSize + inf->pieceSize - 1 ) / inf->pieceSize )
531            return "files";
532    }
533
534    /* get announce or announce-list */
535    if( ( str = getannounce( inf, meta ) ) )
536        return str;
537
538    /* get the url-list */
539    geturllist( inf, meta );
540
541    /* filename of Transmission's copy */
542    tr_free( inf->torrent );
543    inf->torrent = session ?  getTorrentFilename( session, inf ) : NULL;
544
545    return NULL;
546}
547
548tr_bool
549tr_metainfoParse( const tr_session * session,
550                  const tr_benc    * meta_in,
551                  tr_info          * inf,
552                  tr_bool          * hasInfoDict,
553                  int              * infoDictLength )
554{
555    const char * badTag = tr_metainfoParseImpl( session,
556                                                inf,
557                                                hasInfoDict,
558                                                infoDictLength,
559                                                meta_in );
560    const tr_bool success = badTag == NULL;
561
562    if( badTag )
563    {
564        tr_nerr( inf->name, _( "Invalid metadata entry \"%s\"" ), badTag );
565        tr_metainfoFree( inf );
566    }
567
568    return success;
569}
570
571void
572tr_metainfoFree( tr_info * inf )
573{
574    int i;
575    tr_file_index_t ff;
576
577    for( i = 0; i < inf->webseedCount; ++i )
578        tr_free( inf->webseeds[i] );
579
580    for( ff = 0; ff < inf->fileCount; ++ff )
581        tr_free( inf->files[ff].name );
582
583    tr_free( inf->webseeds );
584    tr_free( inf->pieces );
585    tr_free( inf->files );
586    tr_free( inf->comment );
587    tr_free( inf->creator );
588    tr_free( inf->torrent );
589    tr_free( inf->name );
590
591    for( i = 0; i < inf->trackerCount; ++i )
592    {
593        tr_free( inf->trackers[i].announce );
594        tr_free( inf->trackers[i].scrape );
595    }
596    tr_free( inf->trackers );
597
598    memset( inf, '\0', sizeof( tr_info ) );
599}
600
601void
602tr_metainfoRemoveSaved( const tr_session * session,
603                        const tr_info *   inf )
604{
605    char * filename;
606
607    filename = getTorrentFilename( session, inf );
608    unlink( filename );
609    tr_free( filename );
610
611    filename = getOldTorrentFilename( session, inf );
612    unlink( filename );
613    tr_free( filename );
614}
Note: See TracBrowser for help on using the repository browser.