From a6aa88477617ee0c570a3229a220682338834165 Mon Sep 17 00:00:00 2001 From: Eric Petit Date: Thu, 12 Jan 2006 17:43:21 +0000 Subject: [PATCH] Import from 2005-10-26 --- AUTHORS | 28 + Jamfile | 46 + Jamrules | 85 ++ LICENSE | 23 + NEWS | 22 + README | 6 + configure | 85 ++ libtransmission/Jamfile | 8 + libtransmission/bencode.c | 205 +++++ libtransmission/bencode.h | 58 ++ libtransmission/clients.c | 88 ++ libtransmission/clients.h | 23 + libtransmission/fastresume.h | 288 ++++++ libtransmission/fdlimit.c | 279 ++++++ libtransmission/fdlimit.h | 34 + libtransmission/inout.c | 601 +++++++++++++ libtransmission/inout.h | 33 + libtransmission/internal.h | 184 ++++ libtransmission/metainfo.c | 282 ++++++ libtransmission/metainfo.h | 28 + libtransmission/net.c | 225 +++++ libtransmission/net.h | 32 + libtransmission/peer.c | 464 ++++++++++ libtransmission/peer.h | 38 + libtransmission/peermessages.h | 306 +++++++ libtransmission/peerutils.h | 827 ++++++++++++++++++ libtransmission/sha1.c | 235 +++++ libtransmission/sha1.h | 68 ++ libtransmission/tracker.c | 597 +++++++++++++ libtransmission/tracker.h | 36 + libtransmission/transmission.c | 541 ++++++++++++ libtransmission/transmission.h | 210 +++++ libtransmission/upload.c | 136 +++ libtransmission/upload.h | 32 + libtransmission/utils.c | 63 ++ libtransmission/utils.h | 147 ++++ macosx/Controller.h | 78 ++ macosx/Controller.m | 705 +++++++++++++++ macosx/English.lproj/InfoPlist.strings | 4 + macosx/English.lproj/MainMenu.nib/classes.nib | 55 ++ macosx/English.lproj/MainMenu.nib/info.nib | 26 + .../MainMenu.nib/keyedobjects.nib | Bin 0 -> 35882 bytes macosx/Images/Info.tiff | Bin 0 -> 19380 bytes macosx/Images/Open.tiff | Bin 0 -> 19220 bytes macosx/Images/Progress.tiff | Bin 0 -> 26220 bytes macosx/Images/Remove.tiff | Bin 0 -> 19496 bytes macosx/Images/Resume.tiff | Bin 0 -> 19220 bytes macosx/Images/RevealOff.tiff | Bin 0 -> 528 bytes macosx/Images/RevealOn.tiff | Bin 0 -> 478 bytes macosx/Images/Stop.tiff | Bin 0 -> 18956 bytes macosx/Images/Transmission.icns | Bin 0 -> 58576 bytes macosx/Images/TransmissionDocument.icns | Bin 0 -> 44289 bytes macosx/Info.plist.in | 45 + macosx/NameCell.h | 33 + macosx/NameCell.m | 167 ++++ macosx/PrefsController.h | 49 ++ macosx/PrefsController.m | 374 ++++++++ macosx/ProgressCell.h | 43 + macosx/ProgressCell.m | 257 ++++++ macosx/Transmission.xcodeproj/project.pbxproj | 386 ++++++++ macosx/Transmission_Prefix.pch | 3 + macosx/Utils.h | 74 ++ macosx/main.m | 36 + transmissioncli.c | 297 +++++++ 64 files changed, 8995 insertions(+) create mode 100644 AUTHORS create mode 100644 Jamfile create mode 100644 Jamrules create mode 100644 LICENSE create mode 100644 NEWS create mode 100644 README create mode 100755 configure create mode 100644 libtransmission/Jamfile create mode 100644 libtransmission/bencode.c create mode 100644 libtransmission/bencode.h create mode 100644 libtransmission/clients.c create mode 100644 libtransmission/clients.h create mode 100644 libtransmission/fastresume.h create mode 100644 libtransmission/fdlimit.c create mode 100644 libtransmission/fdlimit.h create mode 100644 libtransmission/inout.c create mode 100644 libtransmission/inout.h create mode 100644 libtransmission/internal.h create mode 100644 libtransmission/metainfo.c create mode 100644 libtransmission/metainfo.h create mode 100644 libtransmission/net.c create mode 100644 libtransmission/net.h create mode 100644 libtransmission/peer.c create mode 100644 libtransmission/peer.h create mode 100644 libtransmission/peermessages.h create mode 100644 libtransmission/peerutils.h create mode 100644 libtransmission/sha1.c create mode 100644 libtransmission/sha1.h create mode 100644 libtransmission/tracker.c create mode 100644 libtransmission/tracker.h create mode 100644 libtransmission/transmission.c create mode 100644 libtransmission/transmission.h create mode 100644 libtransmission/upload.c create mode 100644 libtransmission/upload.h create mode 100644 libtransmission/utils.c create mode 100644 libtransmission/utils.h create mode 100644 macosx/Controller.h create mode 100644 macosx/Controller.m create mode 100644 macosx/English.lproj/InfoPlist.strings create mode 100644 macosx/English.lproj/MainMenu.nib/classes.nib create mode 100644 macosx/English.lproj/MainMenu.nib/info.nib create mode 100644 macosx/English.lproj/MainMenu.nib/keyedobjects.nib create mode 100644 macosx/Images/Info.tiff create mode 100644 macosx/Images/Open.tiff create mode 100644 macosx/Images/Progress.tiff create mode 100644 macosx/Images/Remove.tiff create mode 100644 macosx/Images/Resume.tiff create mode 100644 macosx/Images/RevealOff.tiff create mode 100644 macosx/Images/RevealOn.tiff create mode 100644 macosx/Images/Stop.tiff create mode 100644 macosx/Images/Transmission.icns create mode 100644 macosx/Images/TransmissionDocument.icns create mode 100644 macosx/Info.plist.in create mode 100644 macosx/NameCell.h create mode 100644 macosx/NameCell.m create mode 100644 macosx/PrefsController.h create mode 100644 macosx/PrefsController.m create mode 100644 macosx/ProgressCell.h create mode 100644 macosx/ProgressCell.m create mode 100644 macosx/Transmission.xcodeproj/project.pbxproj create mode 100644 macosx/Transmission_Prefix.pch create mode 100644 macosx/Utils.h create mode 100644 macosx/main.m create mode 100644 transmissioncli.c diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 000000000..d3cd5c63d --- /dev/null +++ b/AUTHORS @@ -0,0 +1,28 @@ +Authors: + +Eric Petit + + About everything until now + + +Thanks to: + +vi@nwr.jp + + Free SHA1 implementation (sha1.[ch]) + +Mike Matas + + OS X toolbar icons + +Michael, Omar and Adrien + + Beta testing + +Various people + + Writing http://wiki.theory.org/BitTorrentSpecification + +Ahmad M. Afuni + + NetBSD patch + +Jeremy Messenger + + FreeBSD patch + +Martin Stadtmueller + + Icon tweaking diff --git a/Jamfile b/Jamfile new file mode 100644 index 000000000..85a3e37f0 --- /dev/null +++ b/Jamfile @@ -0,0 +1,46 @@ +SubDir TOP ; + +Main transmissioncli : transmissioncli.c ; +LinkLibraries transmissioncli : libtransmission.a ; +ObjectHdrs transmissioncli.c : $(TOP)/libtransmission ; + +if $(OS) = MACOSX +{ + OSXInfoPlist macosx/Info.plist : macosx/Info.plist.in ; + OSXBundle Transmission.app : libtransmission.a + macosx/Controller.h + macosx/Controller.m + macosx/English.lproj/InfoPlist.strings + macosx/English.lproj/MainMenu.nib + macosx/English.lproj/MainMenu.nib/classes.nib + macosx/English.lproj/MainMenu.nib/info.nib + macosx/English.lproj/MainMenu.nib/keyedobjects.nib + macosx/Images/Info.tiff + macosx/Images/Open.tiff + macosx/Images/Progress.tiff + macosx/Images/Remove.tiff + macosx/Images/Resume.tiff + macosx/Images/RevealOff.tiff + macosx/Images/RevealOn.tiff + macosx/Images/Stop.tiff + macosx/Images/Transmission.icns + macosx/Images/TransmissionDocument.icns + macosx/Info.plist + macosx/NameCell.h + macosx/NameCell.m + macosx/PrefsController.h + macosx/PrefsController.m + macosx/ProgressCell.h + macosx/ProgressCell.m + macosx/main.m + macosx/Transmission.xcodeproj/project.pbxproj + macosx/Transmission_Prefix.pch + macosx/Utils.h ; + + OSXPackage Transmission-$(VERSION_STRING)-OSX.dmg : + Transmission.app ; + NotFile package ; + Depends package : Transmission-$(VERSION_STRING)-OSX.dmg ; +} + +SubInclude TOP libtransmission ; diff --git a/Jamrules b/Jamrules new file mode 100644 index 000000000..1633988a5 --- /dev/null +++ b/Jamrules @@ -0,0 +1,85 @@ +include config.jam ; + +if ! $(DEFINES) +{ + Exit "Please run ./configure first." ; +} + +VERSION_MAJOR = 0 ; +VERSION_MINOR = 3 ; +VERSION_STRING = $(VERSION_MAJOR).$(VERSION_MINOR) ; + +DEFINES += VERSION_MAJOR=$(VERSION_MAJOR) + VERSION_MINOR=$(VERSION_MINOR) + VERSION_STRING=\\\"$(VERSION_STRING)\\\" ; +CCFLAGS = -g -Wall -W ; +OPTIM = -O3 ; +RM = rm -Rf ; + +if $(OS) = MACOSX +{ + # Build universal binaries + CCFLAGS += -isysroot /Developer/SDKs/MacOSX10.4u.sdk + -arch ppc -arch i386 ; + LINKFLAGS += -Wl,-syslibroot,/Developer/SDKs/MacOSX10.4u.sdk + -arch ppc -arch i386 ; + + # Use libtool to build static libraries (ar does not handle + # universal binaries) + RANLIB = ; + NOARUPDATE = 1 ; + actions Archive + { + libtool -static $(>) -o $(<) ; + } + + rule OSXInfoPlist + { + Depends $(1) : $(2) ; + Clean clean : $(1) ; + } + actions OSXInfoPlist + { + sed "s/%%VERSION%%/$(VERSION_STRING)/" < $(2) > $(1) + } + + rule OSXBundle + { + Depends exe : $(1) ; + Depends $(1) : $(2) ; + Clean clean : $(1) macosx/build ; + } + actions OSXBundle + { + $(RM) $(1) && ( cd macosx && xcodebuild ) && \ + mv macosx/build/Debug/Transmission.app $(1) + } + + rule OSXPackage + { + Depends $(1) : $(2) ; + Clean clean : $(1) ; + DoOSXPackage $(1) ; + } + actions DoOSXPackage + { + TMP="Transmission $(VERSION_STRING)" + rm -f $(1) "$TMP" && mkdir "$TMP" && + mkdir "$TMP/Transmission.app" && + ditto Transmission.app "$TMP/Transmission.app" && + ditto AUTHORS "$TMP/AUTHORS.txt" && + ditto LICENSE "$TMP/LICENSE.txt" && + ditto NEWS "$TMP/NEWS.txt" && + ( echo "[InternetShortcut]"; \ + echo "URL=http://transmission.m0k.org/" ) > \ + "$TMP/Homepage.url" && + ( echo "[InternetShortcut]"; \ + echo "URL=http://transmission.m0k.org/forum/" ) > \ + "$TMP/Forums.url" && + ( echo "[InternetShortcut]"; \ + echo "URL=http://transmission.m0k.org/contribute.php" ) > \ + "$TMP/Contribute.url" && + hdiutil create -format UDZO -srcfolder "$TMP" $(1) && + rm -rf "$TMP" + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..848d253fe --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +The Transmission binaries and source code are distributed under the MIT +license. + +----- +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL SIMON TATHAM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +----- diff --git a/NEWS b/NEWS new file mode 100644 index 000000000..2cb4e818f --- /dev/null +++ b/NEWS @@ -0,0 +1,22 @@ +NEWS file for Transmission + +0.3 (2005/10/19) + - Fixed "Sometimes sends incorrect messages and looses peers" + - Fixed "Crashes with many torrents or torrents with many files" + - Enhancements in the "End game" mode + - Is nicer to the trackers + - Asks for the rarest pieces first + - OS X: Universal binary for PPC and x86 + - OS X: Fixed "Progress increases every time I pause then resume" + - OS X: Fixed "Sometimes crashes at exit" + - OS X: Cleaner icon + - OS X: Show all sizes in human readable form + - OS X: Keep downloading in the background when the window is closed + - Miscellaneus bugfixes and internal enhancements + +0.2 (2005/09/22) + - Bugfixes + - OS X: Users can now choose where the downloads are sent + +0.1 (2005/09/15) + - First version diff --git a/README b/README new file mode 100644 index 000000000..80340bd06 --- /dev/null +++ b/README @@ -0,0 +1,6 @@ +README for Transmission +======================= + +This is Transmission, a free BitTorrent client. For more information +(including build instructions), please consult the website: +http://transmission.m0k.org/ diff --git a/configure b/configure new file mode 100755 index 000000000..82b8a42fc --- /dev/null +++ b/configure @@ -0,0 +1,85 @@ +#! /bin/sh + +# For > 2 GB files +DEFINES="_FILE_OFFSET_BITS=64 _LARGEFILE_SOURCE" + +# For asprintf +DEFINES="$DEFINES _GNU_SOURCE" + +# System-specific flags +SYSTEM=`uname -s` +case $SYSTEM in + BeOS) + DEFINES="$DEFINES SYS_BEOS" + + RELEASE=`uname -r` + case $RELEASE in + 6.0|5.0.4) # Zeta or R5 / BONE beta 7 + SYSTEM="$SYSTEM / BONE" + LINKLIBS="$LINKLIBS -lbind -lsocket" + ;; + 5.0*) # R5 / net_server + SYSTEM="$SYSTEM / net_server" + DEFINES="$DEFINES BEOS_NETSERVER" + LINKLIBS="$LINKLIBS -lnet" + ;; + *) + echo "Unsupported BeOS version" + exit 1 ;; + esac + ;; + + Darwin) + DEFINES="$DEFINES SYS_DARWIN" + LINKLIBS="$LINKLIBS -lpthread" + ;; + + FreeBSD) + DEFINES="$DEFINES SYS_FREEBSD" + LINKLIBS="$LINKLIBS -pthread" + ;; + + NetBSD) + DEFINES="$DEFINES SYS_NETBSD" + LINKLIBS="$LINKLIBS -lpthread" + ;; + + Linux) + DEFINES="$DEFINES SYS_LINUX" + LINKLIBS="$LINKLIBS -lpthread" + ;; + + *) + echo "Unsupported operating system" + exit 1 ;; +esac +echo "System: $SYSTEM" + +# Check for OpenSSL +cat > testconf.c << EOF +#include +#include +int main() +{ + SHA1( 0, 0, 0 ); +} +EOF +if cc -o testconf testconf.c -lcrypto > /dev/null 2>&1 +then + echo "OpenSSL: yes" + DEFINES="$DEFINES HAVE_OPENSSL" + LINKLIBS="$LINKLIBS -lcrypto" +else + echo "OpenSSL: no, using built-in SHA1 implementation" +fi +rm -f testconf.c testconf + +# Generate config.jam +rm -f config.jam +cat << EOF > config.jam +DEFINES = $DEFINES ; +LINKLIBS = $LINKLIBS ; +EOF + +echo +echo "To build Transmission, run 'jam'." diff --git a/libtransmission/Jamfile b/libtransmission/Jamfile new file mode 100644 index 000000000..d21df5c75 --- /dev/null +++ b/libtransmission/Jamfile @@ -0,0 +1,8 @@ +SubDir TOP libtransmission ; + +LIBTRANSMISSION_SRC = + transmission.c bencode.c net.c tracker.c peer.c inout.c + metainfo.c sha1.c utils.c upload.c fdlimit.c clients.c ; + +Library libtransmission.a : $(LIBTRANSMISSION_SRC) ; +ObjectDefines $(LIBTRANSMISSION_SRC) : __TRANSMISSION__ ; diff --git a/libtransmission/bencode.c b/libtransmission/bencode.c new file mode 100644 index 000000000..3ee15ae4c --- /dev/null +++ b/libtransmission/bencode.c @@ -0,0 +1,205 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include "transmission.h" + +#define LIST_SIZE 20 + +int _tr_bencLoad( char * buf, benc_val_t * val, char ** end ) +{ + char * p, * foo; + + if( !end ) + { + /* So we only have to check once */ + end = &foo; + } + + val->begin = buf; + + if( buf[0] == 'i' ) + { + /* Integer: i1242e */ + val->type = TYPE_INT; + val->val.i = strtoll( &buf[1], &p, 10 ); + + if( p == &buf[1] || p[0] != 'e' ) + { + return 1; + } + + val->end = p + 1; + } + else if( buf[0] == 'l' || buf[0] == 'd' ) + { + /* List: le + Dict: de + A dictionary is just a special kind of list with an even + count of items, and where even items are strings. */ + char * cur; + char is_dict; + char str_expected; + + is_dict = ( buf[0] == 'd' ); + val->type = is_dict ? TYPE_DICT : TYPE_LIST; + val->val.l.alloc = LIST_SIZE; + val->val.l.count = 0; + val->val.l.vals = malloc( LIST_SIZE * sizeof( benc_val_t ) ); + cur = &buf[1]; + str_expected = 1; + while( cur[0] != 'e' ) + { + if( val->val.l.count == val->val.l.alloc ) + { + /* We need a bigger boat */ + val->val.l.alloc += LIST_SIZE; + val->val.l.vals = realloc( val->val.l.vals, + val->val.l.alloc * sizeof( benc_val_t ) ); + } + if( tr_bencLoad( cur, &val->val.l.vals[val->val.l.count], &p ) ) + { + return 1; + } + if( is_dict && str_expected && + val->val.l.vals[val->val.l.count].type != TYPE_STR ) + { + return 1; + } + str_expected = !str_expected; + + val->val.l.count++; + cur = p; + } + + if( is_dict && ( val->val.l.count & 1 ) ) + { + return 1; + } + + val->end = cur + 1; + } + else + { + /* String: 12:whateverword */ + val->type = TYPE_STR; + val->val.s.i = strtol( buf, &p, 10 ); + + if( p == buf || p[0] != ':' ) + { + return 1; + } + + val->val.s.s = malloc( val->val.s.i + 1 ); + val->val.s.s[val->val.s.i] = 0; + memcpy( val->val.s.s, p + 1, val->val.s.i ); + + val->end = p + 1 + val->val.s.i; + } + + *end = val->end; + + return 0; +} + +static void __bencPrint( benc_val_t * val, int space ) +{ + int i; + + for( i = 0; i < space; i++ ) + { + fprintf( stderr, " " ); + } + + switch( val->type ) + { + case TYPE_INT: + fprintf( stderr, "int: %lld\n", val->val.i ); + break; + + case TYPE_STR: + fprintf( stderr, "%s\n", val->val.s.s ); + break; + + case TYPE_LIST: + fprintf( stderr, "list\n" ); + for( i = 0; i < val->val.l.count; i++ ) + __bencPrint( &val->val.l.vals[i], space + 1 ); + break; + + case TYPE_DICT: + fprintf( stderr, "dict\n" ); + for( i = 0; i < val->val.l.count; i++ ) + __bencPrint( &val->val.l.vals[i], space + 1 ); + break; + } +} + +void tr_bencPrint( benc_val_t * val ) +{ + __bencPrint( val, 0 ); +} + +void tr_bencFree( benc_val_t * val ) +{ + int i; + + switch( val->type ) + { + case TYPE_INT: + break; + + case TYPE_STR: + if( val->val.s.s ) + { + free( val->val.s.s ); + } + break; + + case TYPE_LIST: + case TYPE_DICT: + for( i = 0; i < val->val.l.count; i++ ) + { + tr_bencFree( &val->val.l.vals[i] ); + } + free( val->val.l.vals ); + break; + } +} + +benc_val_t * tr_bencDictFind( benc_val_t * val, char * key ) +{ + int i; + if( val->type != TYPE_DICT ) + { + return NULL; + } + + for( i = 0; i < val->val.l.count; i += 2 ) + { + if( !strcmp( val->val.l.vals[i].val.s.s, key ) ) + { + return &val->val.l.vals[i+1]; + } + } + + return NULL; +} diff --git a/libtransmission/bencode.h b/libtransmission/bencode.h new file mode 100644 index 000000000..32e053658 --- /dev/null +++ b/libtransmission/bencode.h @@ -0,0 +1,58 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#ifndef TR_BENCODE_H +#define TR_BENCODE_H 1 + +typedef struct benc_val_s +{ + char * begin; + char * end; +#define TYPE_INT 1 +#define TYPE_STR 2 +#define TYPE_LIST 4 +#define TYPE_DICT 8 + char type; + union + { + int64_t i; + struct + { + int i; + char * s; + } s; + struct + { + int alloc; + int count; + struct benc_val_s * vals; + } l; + } val; +} benc_val_t; + +#define tr_bencLoad(b,v,e) _tr_bencLoad((char*)(b),v,(char**)e) +int _tr_bencLoad( char * buf, benc_val_t * val, char ** end ); +void tr_bencPrint( benc_val_t * val ); +void tr_bencFree( benc_val_t * val ); +benc_val_t * tr_bencDictFind( benc_val_t * val, char * key ); + +#endif diff --git a/libtransmission/clients.c b/libtransmission/clients.c new file mode 100644 index 000000000..6f544e52e --- /dev/null +++ b/libtransmission/clients.c @@ -0,0 +1,88 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include "transmission.h" + +char * tr_clientForId( uint8_t * id ) +{ + char * ret = NULL; + + if( id[0] == '-' && id[7] == '-' ) + { + if( !memcmp( &id[1], "TR", 2 ) ) + { + asprintf( &ret, "Transmission %d.%d", + ( id[3] - '0' ) * 10 + ( id[4] - '0' ), + ( id[5] - '0' ) * 10 + ( id[6] - '0' ) ); + } + else if( !memcmp( &id[1], "AZ", 2 ) ) + { + asprintf( &ret, "Azureus %c.%c.%c.%c", + id[3], id[4], id[5], id[6] ); + } + else if( !memcmp( &id[1], "TS", 2 ) ) + { + asprintf( &ret, "TorrentStorm (%c%c%c%c)", + id[3], id[4], id[5], id[6] ); + } + else if( !memcmp( &id[1], "BC", 2 ) ) + { + asprintf( &ret, "BitComet %d.%c%c", + ( id[3] - '0' ) * 10 + ( id[4] - '0' ), + id[5], id[6] ); + } + else if( !memcmp( &id[1], "SZ", 2 ) ) + { + asprintf( &ret, "Shareaza %c.%c.%c.%c", + id[3], id[4], id[5], id[6] ); + } + } + else if( !memcmp( &id[4], "----", 4 ) ) + { + if( id[0] == 'T' ) + { + asprintf( &ret, "BitTornado (%c%c%c)", id[1], id[2], id[3] ); + } + else if( id[0] == 'A' ) + { + asprintf( &ret, "ABC (%c%c%c)", id[1], id[2], id[3] ); + } + } + else if( id[0] == 'M' && id[2] == '-' && + id[4] == '-' && id[6] == '-' && + id[7] == '-' ) + { + asprintf( &ret, "BitTorrent %c.%c.%c", id[1], id[3], id[5] ); + } + else if( !memcmp( id, "exbc", 4 ) ) + { + asprintf( &ret, "BitComet %d.%02d", id[4], id[5] ); + } + + if( !ret ) + { + asprintf( &ret, "Unknown client (%c%c%c%c%c%c%c%c)", + id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7] ); + } + + return ret; +} diff --git a/libtransmission/clients.h b/libtransmission/clients.h new file mode 100644 index 000000000..af6e9afad --- /dev/null +++ b/libtransmission/clients.h @@ -0,0 +1,23 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +char * tr_clientForId( uint8_t * ); diff --git a/libtransmission/fastresume.h b/libtransmission/fastresume.h new file mode 100644 index 000000000..8b869fa3f --- /dev/null +++ b/libtransmission/fastresume.h @@ -0,0 +1,288 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +/*********************************************************************** + * Fast resume + *********************************************************************** + * Format of the resume file: + * - 4 bytes: format version (currently 0) + * - 4 bytes * number of files: mtimes of files + * - 1 bit * number of blocks: whether we have the block or not + * - 4 bytes * number of pieces (byte aligned): the pieces that have + * been completed or started in each slot + * + * The resume file is located in ~/.transmission/. Its name is + * "resume.". + * + * All values are stored in the native endianness. Moving a + * libtransmission resume file from an architecture to another will not + * work, although it will not hurt either (the mtimes will be wrong, + * so the files will be scanned). + **********************************************************************/ + +static char * fastResumeFolderName() +{ + char * ret; + asprintf( &ret, "%s/.transmission", getenv( "HOME" ) ); + return ret; +} + +static char * fastResumeFileName( tr_io_t * io ) +{ + char * ret, * p; + int i; + + p = fastResumeFolderName(); + asprintf( &ret, "%s/resume.%40d", p, 0 ); + free( p ); + + p = &ret[ strlen( ret ) - 2 * SHA_DIGEST_LENGTH ]; + for( i = 0; i < SHA_DIGEST_LENGTH; i++ ) + { + sprintf( p, "%02x", io->tor->info.hash[i] ); + p += 2; + } + + return ret; +} + +static int fastResumeMTimes( tr_io_t * io, int * tab ) +{ + tr_torrent_t * tor = io->tor; + tr_info_t * inf = &tor->info; + + int i; + char * path; + struct stat sb; + + for( i = 0; i < inf->fileCount; i++ ) + { + asprintf( &path, "%s/%s", tor->destination, inf->files[i].name ); + if( stat( path, &sb ) ) + { + tr_err( "Could not stat '%s'", path ); + free( path ); + return 1; + } + if( ( sb.st_mode & S_IFMT ) != S_IFREG ) + { + tr_err( "Wrong st_mode for '%s'", path ); + free( path ); + return 1; + } + free( path ); + +#ifdef SYS_DARWIN + tab[i] = ( sb.st_mtimespec.tv_sec & 0x7FFFFFFF ); +#else + tab[i] = ( sb.st_mtime & 0x7FFFFFFF ); +#endif + } + + return 0; +} + +static void fastResumeSave( tr_io_t * io ) +{ + tr_torrent_t * tor = io->tor; + tr_info_t * inf = &tor->info; + + FILE * file; + int version = 0; + char * path; + int * fileMTimes; + int i; + uint8_t * blockBitfield; + + /* Get file sizes */ + fileMTimes = malloc( inf->fileCount * 4 ); + if( fastResumeMTimes( io, fileMTimes ) ) + { + free( fileMTimes ); + return; + } + + /* Create folder if missing */ + path = fastResumeFolderName(); + mkdir( path, 0755 ); + free( path ); + + /* Create/overwrite the resume file */ + path = fastResumeFileName( io ); + if( !( file = fopen( path, "w" ) ) ) + { + tr_err( "Could not open '%s' for writing", path ); + free( fileMTimes ); + free( path ); + return; + } + free( path ); + + /* Write format version */ + fwrite( &version, 4, 1, file ); + + /* Write file mtimes */ + fwrite( fileMTimes, 4, inf->fileCount, file ); + free( fileMTimes ); + + /* Build and write the bitfield for blocks */ + blockBitfield = calloc( ( tor->blockCount + 7 ) / 8, 1 ); + for( i = 0; i < tor->blockCount; i++ ) + { + if( tor->blockHave[i] < 0 ) + { + tr_bitfieldAdd( blockBitfield, i ); + } + } + fwrite( blockBitfield, ( tor->blockCount + 7 ) / 8, 1, file ); + free( blockBitfield ); + + /* Write the 'slotPiece' table */ + fwrite( io->slotPiece, 4, inf->pieceCount, file ); + + fclose( file ); +} + +static int fastResumeLoad( tr_io_t * io ) +{ + tr_torrent_t * tor = io->tor; + tr_info_t * inf = &tor->info; + + FILE * file; + int version = 0; + char * path; + int * fileMTimes1, * fileMTimes2; + int i, j; + uint8_t * blockBitfield; + + int size; + + /* Open resume file */ + path = fastResumeFileName( io ); + if( !( file = fopen( path, "r" ) ) ) + { + tr_inf( "Could not open '%s' for reading", path ); + free( path ); + return 1; + } + free( path ); + + /* Check the size */ + size = 4 + 4 * inf->fileCount + 4 * inf->pieceCount + + ( tor->blockCount + 7 ) / 8; + fseek( file, 0, SEEK_END ); + if( ftell( file ) != size ) + { + tr_inf( "Wrong size for resume file (%d bytes, %d expected)", + ftell( file ), size ); + fclose( file ); + return 1; + } + fseek( file, 0, SEEK_SET ); + + /* Check format version */ + fread( &version, 4, 1, file ); + if( version != 0 ) + { + tr_inf( "Resume file has version %d, not supported", + version ); + fclose( file ); + return 1; + } + + /* Compare file mtimes */ + fileMTimes1 = malloc( inf->fileCount * 4 ); + if( fastResumeMTimes( io, fileMTimes1 ) ) + { + free( fileMTimes1 ); + fclose( file ); + return 1; + } + fileMTimes2 = malloc( inf->fileCount * 4 ); + fread( fileMTimes2, 4, inf->fileCount, file ); + if( memcmp( fileMTimes1, fileMTimes2, inf->fileCount * 4 ) ) + { + tr_inf( "File mtimes don't match" ); + free( fileMTimes1 ); + free( fileMTimes2 ); + fclose( file ); + return 1; + } + free( fileMTimes1 ); + free( fileMTimes2 ); + + /* Load the bitfield for blocks and fill blockHave */ + blockBitfield = calloc( ( tor->blockCount + 7 ) / 8, 1 ); + fread( blockBitfield, ( tor->blockCount + 7 ) / 8, 1, file ); + tor->blockHaveCount = 0; + for( i = 0; i < tor->blockCount; i++ ) + { + if( tr_bitfieldHas( blockBitfield, i ) ) + { + tor->blockHave[i] = -1; + (tor->blockHaveCount)++; + } + } + free( blockBitfield ); + + /* Load the 'slotPiece' table */ + fread( io->slotPiece, 4, inf->pieceCount, file ); + + fclose( file ); + + /* Update io->pieceSlot, io->slotsUsed, and tor->bitfield */ + io->slotsUsed = 0; + for( i = 0; i < inf->pieceCount; i++ ) + { + io->pieceSlot[i] = -1; + for( j = 0; j < inf->pieceCount; j++ ) + { + if( io->slotPiece[j] == i ) + { + // tr_dbg( "Has piece %d in slot %d", i, j ); + io->pieceSlot[i] = j; + io->slotsUsed = MAX( io->slotsUsed, j + 1 ); + break; + } + } + + for( j = tr_pieceStartBlock( i ); + j < tr_pieceStartBlock( i ) + tr_pieceCountBlocks( i ); + j++ ) + { + if( tor->blockHave[j] > -1 ) + { + break; + } + } + if( j >= tr_pieceStartBlock( i ) + tr_pieceCountBlocks( i ) ) + { + // tr_dbg( "Piece %d is complete", i ); + tr_bitfieldAdd( tor->bitfield, i ); + } + } + // tr_dbg( "Slot used: %d", io->slotsUsed ); + + tr_inf( "Fast resuming successful" ); + + return 0; +} diff --git a/libtransmission/fdlimit.c b/libtransmission/fdlimit.c new file mode 100644 index 000000000..7cac63267 --- /dev/null +++ b/libtransmission/fdlimit.c @@ -0,0 +1,279 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include "transmission.h" + +#define TR_MAX_OPEN_FILES 16 /* That is, real files, not sockets */ +#define TR_RESERVED_FDS 16 /* Number of sockets reserved for + connections to trackers */ + +typedef struct tr_openFile_s +{ + char path[MAX_PATH_LENGTH]; + FILE * file; + +#define STATUS_INVALID 1 +#define STATUS_UNUSED 2 +#define STATUS_USED 4 + int status; + + uint64_t date; + +} tr_openFile_t; + +struct tr_fd_s +{ + tr_lock_t lock; + + int reserved; + + int normal; + int normalMax; + + tr_openFile_t open[TR_MAX_OPEN_FILES]; +}; + +/*********************************************************************** + * tr_fdInit + **********************************************************************/ +tr_fd_t * tr_fdInit() +{ + tr_fd_t * f; + int i, j, s[4096]; + + f = calloc( sizeof( tr_fd_t ), 1 ); + + /* Init lock */ + tr_lockInit( &f->lock ); + + /* Detect the maximum number of open files or sockets */ + for( i = 0; i < 4096; i++ ) + { + if( ( s[i] = socket( AF_INET, SOCK_STREAM, 0 ) ) < 0 ) + { + break; + } + } + for( j = 0; j < i; j++ ) + { + tr_netClose( s[j] ); + } + + tr_dbg( "%d usable file descriptors", i ); + + f->reserved = 0; + f->normal = 0; + + f->normalMax = i - TR_RESERVED_FDS - 10; + /* To be safe, in case the UI needs to write a preferences file + or something */ + + for( i = 0; i < TR_MAX_OPEN_FILES; i++ ) + { + f->open[i].status = STATUS_INVALID; + } + + return f; +} + +/*********************************************************************** + * tr_fdFileOpen + **********************************************************************/ +FILE * tr_fdFileOpen( tr_fd_t * f, char * path ) +{ + int i, winner; + uint64_t date; + + tr_lockLock( f->lock ); + + /* Is it already open? */ + for( i = 0; i < TR_MAX_OPEN_FILES; i++ ) + { + if( f->open[i].status > STATUS_INVALID && + !strcmp( path, f->open[i].path ) ) + { + winner = i; + goto done; + } + } + + /* Can we open one more file? */ + for( i = 0; i < TR_MAX_OPEN_FILES; i++ ) + { + if( f->open[i].status & STATUS_INVALID ) + { + winner = i; + goto open; + } + } + + for( ;; ) + { + /* Close the oldest currently unused file */ + date = tr_date() + 1; + winner = -1; + + for( i = 0; i < TR_MAX_OPEN_FILES; i++ ) + { + if( f->open[i].status & STATUS_USED ) + { + continue; + } + if( f->open[i].date < date ) + { + winner = i; + date = f->open[i].date; + } + } + + if( winner >= 0 ) + { + tr_dbg( "Closing %s", f->open[winner].path ); + fclose( f->open[winner].file ); + goto open; + } + + /* All used! Wait a bit and try again */ + tr_lockUnlock( f->lock ); + tr_wait( 10 ); + tr_lockLock( f->lock ); + } + +open: + tr_dbg( "Opening %s", path ); + snprintf( f->open[winner].path, MAX_PATH_LENGTH, "%s", path ); + f->open[winner].file = fopen( path, "r+" ); + +done: + f->open[winner].status = STATUS_USED; + f->open[winner].date = tr_date(); + tr_lockUnlock( f->lock ); + + return f->open[winner].file; +} + +/*********************************************************************** + * tr_fdFileRelease + **********************************************************************/ +void tr_fdFileRelease( tr_fd_t * f, FILE * file ) +{ + int i; + tr_lockLock( f->lock ); + + for( i = 0; i < TR_MAX_OPEN_FILES; i++ ) + { + if( f->open[i].file == file ) + { + f->open[i].status = STATUS_UNUSED; + break; + } + } + + tr_lockUnlock( f->lock ); +} + +/*********************************************************************** + * tr_fdFileClose + **********************************************************************/ +void tr_fdFileClose( tr_fd_t * f, char * path ) +{ + int i; + + tr_lockLock( f->lock ); + + /* Is it already open? */ + for( i = 0; i < TR_MAX_OPEN_FILES; i++ ) + { + if( f->open[i].status & STATUS_INVALID ) + { + continue; + } + if( !strcmp( path, f->open[i].path ) ) + { + tr_dbg( "Closing %s", path ); + fclose( f->open[i].file ); + f->open[i].status = STATUS_INVALID; + break; + } + } + + tr_lockUnlock( f->lock ); +} + +int tr_fdSocketWillCreate( tr_fd_t * f, int reserved ) +{ + int ret; + + tr_lockLock( f->lock ); + + if( reserved ) + { + if( f->reserved < TR_RESERVED_FDS ) + { + ret = 0; + (f->reserved)++; + } + else + { + ret = 1; + } + } + else + { + if( f->normal < f->normalMax ) + { + ret = 0; + (f->normal)++; + } + else + { + ret = 1; + } + } + + tr_lockUnlock( f->lock ); + + return ret; +} + +void tr_fdSocketClosed( tr_fd_t * f, int reserved ) +{ + tr_lockLock( f->lock ); + + if( reserved ) + { + (f->reserved)--; + } + else + { + (f->normal)--; + } + + tr_lockUnlock( f->lock ); +} + +void tr_fdClose( tr_fd_t * f ) +{ + tr_lockClose( f->lock ); + free( f ); +} + diff --git a/libtransmission/fdlimit.h b/libtransmission/fdlimit.h new file mode 100644 index 000000000..435c5afc3 --- /dev/null +++ b/libtransmission/fdlimit.h @@ -0,0 +1,34 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +typedef struct tr_fd_s tr_fd_t; + +tr_fd_t * tr_fdInit(); + +FILE * tr_fdFileOpen ( tr_fd_t *, char * ); +void tr_fdFileRelease ( tr_fd_t *, FILE * ); +void tr_fdFileClose ( tr_fd_t *, char * ); + +int tr_fdSocketWillCreate ( tr_fd_t *, int ); +void tr_fdSocketClosed ( tr_fd_t *, int ); + +void tr_fdClose ( tr_fd_t * ); diff --git a/libtransmission/inout.c b/libtransmission/inout.c new file mode 100644 index 000000000..9deb71c1f --- /dev/null +++ b/libtransmission/inout.c @@ -0,0 +1,601 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include "transmission.h" + +#ifdef SYS_BEOS +# define fseeko _fseek +#endif + +struct tr_io_s +{ + tr_torrent_t * tor; + + /* Position of pieces + -1 = we haven't started to download this piece yet + n = we have started or completed the piece in slot n */ + int * pieceSlot; + + /* Pieces in slot + -1 = unused slot + n = piece n */ + int * slotPiece; + + int slotsUsed; +}; + +#include "fastresume.h" + +/*********************************************************************** + * Local prototypes + **********************************************************************/ +static int createFiles( tr_io_t * ); +static int checkFiles( tr_io_t * ); +static void closeFiles( tr_io_t * ); +static int readOrWriteBytes( tr_io_t *, uint64_t, int, uint8_t *, int ); +static int readOrWriteSlot( tr_io_t * io, int slot, uint8_t * buf, + int * size, int write ); +static void findSlotForPiece( tr_io_t *, int ); + +#define readBytes(io,o,s,b) readOrWriteBytes(io,o,s,b,0) +#define writeBytes(io,o,s,b) readOrWriteBytes(io,o,s,b,1) + +#define readSlot(io,sl,b,s) readOrWriteSlot(io,sl,b,s,0) +#define writeSlot(io,sl,b,s) readOrWriteSlot(io,sl,b,s,1) + +/*********************************************************************** + * tr_ioInit + *********************************************************************** + * Open all files we are going to write to + **********************************************************************/ +tr_io_t * tr_ioInit( tr_torrent_t * tor ) +{ + tr_io_t * io; + + io = malloc( sizeof( tr_io_t ) ); + io->tor = tor; + + if( createFiles( io ) || checkFiles( io ) ) + { + free( io ); + return NULL; + } + + return io; +} + +/*********************************************************************** + * tr_ioRead + *********************************************************************** + * + **********************************************************************/ +int tr_ioRead( tr_io_t * io, int index, int begin, int length, + uint8_t * buf ) +{ + uint64_t offset; + tr_info_t * inf = &io->tor->info; + + offset = (uint64_t) io->pieceSlot[index] * + (uint64_t) inf->pieceSize + (uint64_t) begin; + + return readBytes( io, offset, length, buf ); +} + +/*********************************************************************** + * tr_ioWrite + *********************************************************************** + * + **********************************************************************/ +int tr_ioWrite( tr_io_t * io, int index, int begin, int length, + uint8_t * buf ) +{ + tr_torrent_t * tor = io->tor; + tr_info_t * inf = &io->tor->info; + uint64_t offset; + int i; + uint8_t hash[SHA_DIGEST_LENGTH]; + uint8_t * pieceBuf; + int pieceSize; + int startBlock, endBlock; + + if( io->pieceSlot[index] < 0 ) + { + findSlotForPiece( io, index ); + tr_inf( "Piece %d: starting in slot %d", index, + io->pieceSlot[index] ); + } + + offset = (uint64_t) io->pieceSlot[index] * + (uint64_t) inf->pieceSize + (uint64_t) begin; + + if( writeBytes( io, offset, length, buf ) ) + { + return 1; + } + + startBlock = tr_pieceStartBlock( index ); + endBlock = startBlock + tr_pieceCountBlocks( index ); + for( i = startBlock; i < endBlock; i++ ) + { + if( tor->blockHave[i] >= 0 ) + { + /* The piece is not complete */ + return 0; + } + } + + /* The piece is complete, check the hash */ + pieceSize = tr_pieceSize( index ); + pieceBuf = malloc( pieceSize ); + readBytes( io, (uint64_t) io->pieceSlot[index] * + (uint64_t) inf->pieceSize, pieceSize, pieceBuf ); + SHA1( pieceBuf, pieceSize, hash ); + free( pieceBuf ); + + if( memcmp( hash, &inf->pieces[20*index], SHA_DIGEST_LENGTH ) ) + { + tr_inf( "Piece %d (slot %d): hash FAILED", index, + io->pieceSlot[index] ); + + /* We will need to reload the whole piece */ + for( i = startBlock; i < endBlock; i++ ) + { + tor->blockHave[i] = 0; + tor->blockHaveCount -= 1; + } + } + else + { + tr_inf( "Piece %d (slot %d): hash OK", index, + io->pieceSlot[index] ); + tr_bitfieldAdd( tor->bitfield, index ); + } + + return 0; +} + +void tr_ioClose( tr_io_t * io ) +{ + closeFiles( io ); + + fastResumeSave( io ); + + free( io->pieceSlot ); + free( io->slotPiece ); + free( io ); +} + +/*********************************************************************** + * createFiles + *********************************************************************** + * Make sure the existing folders/files have correct types and + * permissions, and create missing folders and files + **********************************************************************/ +static int createFiles( tr_io_t * io ) +{ + tr_torrent_t * tor = io->tor; + tr_info_t * inf = &tor->info; + + int i; + char * path, * p; + struct stat sb; + FILE * file; + + tr_dbg( "Creating files..." ); + + for( i = 0; i < inf->fileCount; i++ ) + { + asprintf( &path, "%s/%s", tor->destination, inf->files[i].name ); + + /* Create folders */ + p = path; + while( ( p = strchr( p, '/' ) ) ) + { + *p = '\0'; + if( stat( path, &sb ) ) + { + /* Folder doesn't exist yet */ + mkdir( path, 0755 ); + } + else if( ( sb.st_mode & S_IFMT ) != S_IFDIR ) + { + /* Node exists but isn't a folder */ + printf( "Remove %s, it's in the way.\n", path ); + free( path ); + return 1; + } + *p = '/'; + p++; + } + + if( stat( path, &sb ) ) + { + /* File doesn't exist yet */ + if( !( file = fopen( path, "w" ) ) ) + { + tr_err( "Could not create `%s' (%s)", path, + strerror( errno ) ); + free( path ); + return 1; + } + fclose( file ); + } + else if( ( sb.st_mode & S_IFMT ) != S_IFREG ) + { + /* Node exists but isn't a file */ + printf( "Remove %s, it's in the way.\n", path ); + free( path ); + return 1; + } + + free( path ); + } + + return 0; +} + +/*********************************************************************** + * checkFiles + *********************************************************************** + * Look for pieces + **********************************************************************/ +static int checkFiles( tr_io_t * io ) +{ + tr_torrent_t * tor = io->tor; + tr_info_t * inf = &tor->info; + + int i; + uint8_t * buf; + uint8_t hash[SHA_DIGEST_LENGTH]; + int startBlock, endBlock; + + io->pieceSlot = malloc( inf->pieceCount * sizeof( int ) ); + io->slotPiece = malloc( inf->pieceCount * sizeof( int ) ); + + if( !fastResumeLoad( io ) ) + { + return 0; + } + + tr_dbg( "Checking pieces..." ); + + /* Yet we don't have anything */ + memset( io->pieceSlot, 0xFF, inf->pieceCount * sizeof( int ) ); + memset( io->slotPiece, 0xFF, inf->pieceCount * sizeof( int ) ); + memset( tor->bitfield, 0, ( inf->pieceCount + 7 ) / 8 ); + memset( tor->blockHave, 0, tor->blockCount ); + tor->blockHaveCount = 0; + + /* Check pieces */ + io->slotsUsed = 0; + buf = malloc( inf->pieceSize ); + for( i = 0; i < inf->pieceCount; i++ ) + { + int size, j; + + if( readSlot( io, i, buf, &size ) ) + { + break; + } + + io->slotsUsed = i + 1; + SHA1( buf, size, hash ); + + for( j = i; j < inf->pieceCount - 1; j++ ) + { + if( !memcmp( hash, &inf->pieces[20*j], SHA_DIGEST_LENGTH ) ) + { + int k; + io->pieceSlot[j] = i; + io->slotPiece[i] = j; + tr_bitfieldAdd( tor->bitfield, j ); + + startBlock = tr_pieceStartBlock( j ); + endBlock = startBlock + tr_pieceCountBlocks( j ); + for( k = startBlock; k < endBlock; k++ ) + { + tor->blockHave[k] = -1; + tor->blockHaveCount++; + } + break; + } + } + + if( io->slotPiece[i] > -1 ) + { + continue; + } + + /* Special case for the last piece */ + SHA1( buf, tr_pieceSize( inf->pieceCount - 1 ), hash ); + if( !memcmp( hash, &inf->pieces[20 * (inf->pieceCount - 1)], + SHA_DIGEST_LENGTH ) ) + { + io->pieceSlot[inf->pieceCount - 1] = i; + io->slotPiece[i] = inf->pieceCount - 1; + tr_bitfieldAdd( tor->bitfield, inf->pieceCount - 1 ); + + startBlock = tr_pieceStartBlock( inf->pieceCount - 1 ); + endBlock = startBlock + + tr_pieceCountBlocks( inf->pieceCount - 1 ); + for( j = startBlock; j < endBlock; j++ ) + { + tor->blockHave[j] = -1; + tor->blockHaveCount++; + } + } + } + free( buf ); + + return 0; +} + +/*********************************************************************** + * closeFiles + **********************************************************************/ +static void closeFiles( tr_io_t * io ) +{ + tr_torrent_t * tor = io->tor; + tr_info_t * inf = &tor->info; + + int i; + char * path; + + for( i = 0; i < inf->fileCount; i++ ) + { + asprintf( &path, "%s/%s", tor->destination, inf->files[i].name ); + tr_fdFileClose( tor->fdlimit, path ); + free( path ); + } +} + +/*********************************************************************** + * readOrWriteBytes + *********************************************************************** + * + **********************************************************************/ +static int readOrWriteBytes( tr_io_t * io, uint64_t offset, int size, + uint8_t * buf, int write ) +{ + tr_torrent_t * tor = io->tor; + tr_info_t * inf = &tor->info; + + int piece = offset / inf->pieceSize; + int begin = offset % inf->pieceSize; + + int i; + uint64_t foo; + uint64_t posInFile = 0; + int willRead; + char * path; + FILE * file; + + /* We can't ever read or write more than a piece at a time */ + if( tr_pieceSize( piece ) < begin + size ) + { + return 1; + } + + /* Find which file we shall start reading/writing in */ + foo = 0; + for( i = 0; i < inf->fileCount; i++ ) + { + if( offset < foo + inf->files[i].length ) + { + posInFile = offset - foo; + break; + } + foo += inf->files[i].length; + } + + while( size > 0 ) + { + asprintf( &path, "%s/%s", tor->destination, inf->files[i].name ); + file = tr_fdFileOpen( tor->fdlimit, path ); + free( path ); + + if( !file ) + { + return 1; + } + + willRead = MIN( inf->files[i].length - posInFile, + (uint64_t) size ); + + if( fseeko( file, posInFile, SEEK_SET ) ) + { + return 1; + } + if( write ) + { + if( fwrite( buf, willRead, 1, file ) != 1 ) + { + return 1; + } + } + else + { + if( fread( buf, willRead, 1, file ) != 1 ) + { + return 1; + } + } + + tr_fdFileRelease( tor->fdlimit, file ); + + /* 'willRead' less bytes to do */ + size -= willRead; + buf += willRead; + + /* Go to the beginning of the next file */ + i += 1; + posInFile = 0; + } + + return 0; +} + +/*********************************************************************** + * readSlot + *********************************************************************** + * + **********************************************************************/ +static int readOrWriteSlot( tr_io_t * io, int slot, uint8_t * buf, + int * size, int write ) +{ + tr_torrent_t * tor = io->tor; + tr_info_t * inf = &tor->info; + + uint64_t offset = (uint64_t) slot * (uint64_t) inf->pieceSize; + + *size = 0; + if( slot == inf->pieceCount - 1 ) + { + *size = inf->totalSize % inf->pieceSize; + } + if( !*size ) + { + *size = inf->pieceSize; + } + + return readOrWriteBytes( io, offset, *size, buf, write ); +} + +static void invertSlots( tr_io_t * io, int slot1, int slot2 ) +{ + tr_torrent_t * tor = io->tor; + tr_info_t * inf = &tor->info; + + uint8_t * buf1, * buf2; + int piece1, piece2, foo; + + buf1 = calloc( inf->pieceSize, 1 ); + buf2 = calloc( inf->pieceSize, 1 ); + + readSlot( io, slot1, buf1, &foo ); + readSlot( io, slot2, buf2, &foo ); + + writeSlot( io, slot1, buf2, &foo ); + writeSlot( io, slot2, buf1, &foo ); + + free( buf1 ); + free( buf2 ); + + piece1 = io->slotPiece[slot1]; + piece2 = io->slotPiece[slot2]; + io->slotPiece[slot1] = piece2; + io->slotPiece[slot2] = piece1; + if( piece1 >= 0 ) + { + io->pieceSlot[piece1] = slot2; + } + if( piece2 >= 0 ) + { + io->pieceSlot[piece2] = slot1; + } +} + +static void reorderPieces( tr_io_t * io ) +{ + tr_torrent_t * tor = io->tor; + tr_info_t * inf = &tor->info; + + int i, didInvert; + + /* Try to move pieces to their final places */ + do + { + didInvert = 0; + + for( i = 0; i < inf->pieceCount; i++ ) + { + if( io->pieceSlot[i] < 0 ) + { + /* We haven't started this piece yet */ + continue; + } + if( io->pieceSlot[i] == i ) + { + /* Already in place */ + continue; + } + if( i >= io->slotsUsed ) + { + /* File is not big enough yet */ + continue; + } + + /* Move piece i into slot i */ + tr_inf( "invert %d and %d", io->pieceSlot[i], i ); + invertSlots( io, i, io->pieceSlot[i] ); + didInvert = 1; + } + } while( didInvert ); +} + +static void findSlotForPiece( tr_io_t * io, int piece ) +{ + int i; +#if 0 + tr_torrent_t * tor = io->tor; + tr_info_t * inf = &tor->info; + + tr_dbg( "Entering findSlotForPiece" ); + + for( i = 0; i < inf->pieceCount; i++ ) + printf( "%02d ", io->slotPiece[i] ); + printf( "\n" ); + for( i = 0; i < inf->pieceCount; i++ ) + printf( "%02d ", io->pieceSlot[i] ); + printf( "\n" ); +#endif + + /* Look for an empty slot somewhere */ + for( i = 0; i < io->slotsUsed; i++ ) + { + if( io->slotPiece[i] < 0 ) + { + io->pieceSlot[piece] = i; + io->slotPiece[i] = piece; + goto reorder; + } + } + + /* No empty slot, extend the file */ + io->pieceSlot[piece] = io->slotsUsed; + io->slotPiece[io->slotsUsed] = piece; + (io->slotsUsed)++; + + reorder: + reorderPieces( io ); + +#if 0 + for( i = 0; i < inf->pieceCount; i++ ) + printf( "%02d ", io->slotPiece[i] ); + printf( "\n" ); + for( i = 0; i < inf->pieceCount; i++ ) + printf( "%02d ", io->pieceSlot[i] ); + printf( "\n" ); + + printf( "Leaving findSlotForPiece\n" ); +#endif +} diff --git a/libtransmission/inout.h b/libtransmission/inout.h new file mode 100644 index 000000000..5de6fdfe4 --- /dev/null +++ b/libtransmission/inout.h @@ -0,0 +1,33 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#ifndef TR_IO_H +#define TR_IO_H 1 + +typedef struct tr_io_s tr_io_t; + +tr_io_t * tr_ioInit ( tr_torrent_t * ); +int tr_ioRead ( tr_io_t *, int, int, int, uint8_t * ); +int tr_ioWrite ( tr_io_t *, int, int, int, uint8_t * ); +void tr_ioClose ( tr_io_t * ); + +#endif diff --git a/libtransmission/internal.h b/libtransmission/internal.h new file mode 100644 index 000000000..7c63b45e7 --- /dev/null +++ b/libtransmission/internal.h @@ -0,0 +1,184 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#ifndef TR_INTERNAL_H +#define TR_INTERNAL_H 1 + +/* Standard headers used here and there. + That is probably ugly to put them all here, but it is sooo + convenient */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef BEOS_NETSERVER +# define in_port_t uint16_t +#else +# include +#endif + +/* We use OpenSSL whenever possible, since it is likely to be more + optimized and it is ok to use it with a MIT-licensed application. + Otherwise, we use the included implementation by vi@nwr.jp. */ +#ifdef HAVE_OPENSSL +# undef SHA_DIGEST_LENGTH +# include +#else +# include "sha1.h" +# define SHA1(p,i,h) \ + { \ + sha1_state_s pms; \ + sha1_init( &pms ); \ + sha1_update( &pms, (sha1_byte_t *) p, i ); \ + sha1_finish( &pms, (sha1_byte_t *) h ); \ + } +#endif + +/* Convenient macros to perform uint32_t endian conversions with + char pointers */ +#define TR_NTOHL(p,a) (a) = ntohl(*((uint32_t*)(p))) +#define TR_HTONL(a,p) *((uint32_t*)(p)) = htonl((a)) + +/* Multithreading support: native threads on BeOS, pthreads elsewhere */ +#ifdef SYS_BEOS +# include +# define tr_thread_t thread_id +# define tr_threadCreate(pt,f,d) *(pt) = spawn_thread((void*)f,"",10,d); \ + resume_thread(*(pt)); +# define tr_threadJoin(t) { long e; wait_for_thread(t,&e); } +# define tr_lock_t sem_id +# define tr_lockInit(pl) *(pl) = create_sem(1,"") +# define tr_lockLock(l) acquire_sem(l) +# define tr_lockUnlock(l) release_sem(l) +# define tr_lockClose(l) delete_sem(l) +#else +# include +# define tr_thread_t pthread_t +# define tr_threadCreate(pt,f,d) pthread_create(pt,NULL,(void*)f,d) +# define tr_threadJoin(t) pthread_join(t,NULL) +# define tr_lock_t pthread_mutex_t +# define tr_lockInit(pl) pthread_mutex_init(pl,NULL) +# define tr_lockLock(l) pthread_mutex_lock(&l) +# define tr_lockUnlock(l) pthread_mutex_unlock(&l) +# define tr_lockClose(l) pthread_mutex_destroy(&l) +#endif + +/* Sometimes the system defines MAX/MIN, sometimes not. In the latter + case, define those here since we will use them */ +#ifndef MAX +#define MAX(a,b) ((a)>(b)?(a):(b)) +#endif +#ifndef MIN +#define MIN(a,b) ((a)>(b)?(b):(a)) +#endif + +#define TR_MAX_PEER_COUNT 60 + +typedef struct tr_torrent_s tr_torrent_t; + +#include "bencode.h" +#include "metainfo.h" +#include "tracker.h" +#include "peer.h" +#include "net.h" +#include "inout.h" +#include "upload.h" +#include "fdlimit.h" +#include "clients.h" + +struct tr_torrent_s +{ + tr_info_t info; + + tr_upload_t * upload; + tr_fd_t * fdlimit; + + int status; + char error[128]; + + char * id; + + /* An escaped string used to include the hash in HTTP queries */ + char hashString[3*SHA_DIGEST_LENGTH+1]; + + char scrape[MAX_PATH_LENGTH]; + + /* Where to download */ + char * destination; + + /* How many bytes we ask for per request */ + int blockSize; + int blockCount; + + /* Status for each block + -1 = we have it + n = we are downloading it from n peers */ + char * blockHave; + int blockHaveCount; + uint8_t * bitfield; + + volatile char die; + tr_thread_t thread; + tr_lock_t lock; + + tr_tracker_t * tracker; + tr_io_t * io; + + int bindSocket; + int bindPort; + int peerCount; + tr_peer_t * peers[TR_MAX_PEER_COUNT]; + + uint64_t dates[10]; + uint64_t downloaded[10]; + uint64_t uploaded[10]; +}; + +#include "utils.h" + +struct tr_handle_s +{ + int torrentCount; + tr_torrent_t * torrents[TR_MAX_TORRENT_COUNT]; + + tr_upload_t * upload; + tr_fd_t * fdlimit; + + int bindPort; + + char id[21]; +}; + +#endif diff --git a/libtransmission/metainfo.c b/libtransmission/metainfo.c new file mode 100644 index 000000000..a0b76c9ac --- /dev/null +++ b/libtransmission/metainfo.c @@ -0,0 +1,282 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include "transmission.h" + +/*********************************************************************** + * Local prototypes + **********************************************************************/ +static void strcatUTF8( char *, char * ); + +/*********************************************************************** + * tr_metainfoParse + *********************************************************************** + * + **********************************************************************/ +int tr_metainfoParse( tr_info_t * inf, const char * path ) +{ + FILE * file; + char * buf; + benc_val_t meta, * beInfo, * list, * val; + char * s, * s2, * s3; + int i; + struct stat sb; + + snprintf( inf->torrent, MAX_PATH_LENGTH, path ); + + if( stat( path, &sb ) ) + { + fprintf( stderr, "Could not stat file (%s)\n", path ); + return 1; + } + if( ( sb.st_mode & S_IFMT ) != S_IFREG ) + { + fprintf( stderr, "Not a regular file (%s)\n", path ); + return 1; + } + if( sb.st_size > 2097152 ) + { + tr_err( "Torrent file is too big (%d bytes)", sb.st_size ); + return 1; + } + + /* Load the torrent file into our buffer */ + file = fopen( path, "rb" ); + if( !file ) + { + fprintf( stderr, "Could not open file (%s)\n", path ); + return 1; + } + buf = malloc( sb.st_size ); + fseek( file, 0, SEEK_SET ); + if( fread( buf, sb.st_size, 1, file ) != 1 ) + { + fprintf( stderr, "Read error (%s)\n", path ); + free( buf ); + fclose( file ); + return 1; + } + fclose( file ); + + /* Parse bencoded infos */ + if( tr_bencLoad( buf, &meta, NULL ) ) + { + fprintf( stderr, "Error while parsing bencoded data\n" ); + free( buf ); + return 1; + } + + /* Get info hash */ + if( !( beInfo = tr_bencDictFind( &meta, "info" ) ) ) + { + fprintf( stderr, "Could not find \"info\" dictionary\n" ); + tr_bencFree( &meta ); + free( buf ); + return 1; + } + SHA1( (uint8_t *) beInfo->begin, + (long) beInfo->end - (long) beInfo->begin, inf->hash ); + + /* No that we got the hash, we won't need this anymore */ + free( buf ); + + if( !( val = tr_bencDictFind( &meta, "announce" ) ) ) + { + fprintf( stderr, "No \"announce\" entry\n" ); + tr_bencFree( &meta ); + return 1; + } + + /* Skip spaces */ + s3 = val->val.s.s; + while( *s3 && *s3 == ' ' ) + { + s3++; + } + + /* Parse announce URL */ + if( strncmp( s3, "http://", 7 ) ) + { + fprintf( stderr, "Invalid announce URL (%s)\n", + inf->trackerAddress ); + tr_bencFree( &meta ); + return 1; + } + s = strchr( s3 + 7, ':' ); + s2 = strchr( s3 + 7, '/' ); + if( s && s < s2 ) + { + memcpy( inf->trackerAddress, s3 + 7, + (long) s - (long) s3 - 7 ); + inf->trackerPort = atoi( s + 1 ); + } + else if( s2 ) + { + memcpy( inf->trackerAddress, s3 + 7, + (long) s2 - (long) s3 - 7 ); + inf->trackerPort = 80; + } + else + { + fprintf( stderr, "Invalid announce URL (%s)\n", + inf->trackerAddress ); + tr_bencFree( &meta ); + return 1; + } + snprintf( inf->trackerAnnounce, MAX_PATH_LENGTH, s2 ); + + /* Piece length */ + if( !( val = tr_bencDictFind( beInfo, "piece length" ) ) ) + { + fprintf( stderr, "No \"piece length\" entry\n" ); + tr_bencFree( &meta ); + return 1; + } + inf->pieceSize = val->val.i; + + /* Hashes */ + val = tr_bencDictFind( beInfo, "pieces" ); + if( val->val.s.i % SHA_DIGEST_LENGTH ) + { + fprintf( stderr, "Invalid \"piece\" string (size is %d)\n", + val->val.s.i ); + return 1; + } + inf->pieceCount = val->val.s.i / SHA_DIGEST_LENGTH; + inf->pieces = (uint8_t *) val->val.s.s; /* Ugly, but avoids a memcpy */ + val->val.s.s = NULL; + + /* TODO add more tests so we don't crash on weird files */ + + inf->totalSize = 0; + if( ( list = tr_bencDictFind( beInfo, "files" ) ) ) + { + /* Multi-file mode */ + int j; + + val = tr_bencDictFind( beInfo, "name" ); + strcatUTF8( inf->name, val->val.s.s ); + + inf->fileCount = list->val.l.count; + inf->files = calloc( inf->fileCount * sizeof( tr_file_t ), 1 ); + + for( i = 0; i < list->val.l.count; i++ ) + { + val = tr_bencDictFind( &list->val.l.vals[i], "path" ); + strcatUTF8( inf->files[i].name, inf->name ); + for( j = 0; j < val->val.l.count; j++ ) + { + strcatUTF8( inf->files[i].name, "/" ); + strcatUTF8( inf->files[i].name, + val->val.l.vals[j].val.s.s ); + } + val = tr_bencDictFind( &list->val.l.vals[i], "length" ); + inf->files[i].length = val->val.i; + inf->totalSize += val->val.i; + } + + } + else + { + /* Single-file mode */ + inf->fileCount = 1; + inf->files = calloc( sizeof( tr_file_t ), 1 ); + + val = tr_bencDictFind( beInfo, "name" ); + strcatUTF8( inf->files[0].name, val->val.s.s ); + strcatUTF8( inf->name, val->val.s.s ); + + val = tr_bencDictFind( beInfo, "length" ); + inf->files[0].length = val->val.i; + inf->totalSize += val->val.i; + } + + if( (uint64_t) inf->pieceCount != + ( inf->totalSize + inf->pieceSize - 1 ) / inf->pieceSize ) + { + fprintf( stderr, "Size of hashes and files don't match\n" ); + tr_bencFree( &meta ); + return 1; + } + + tr_bencFree( &meta ); + return 0; +} + +/*********************************************************************** + * strcatUTF8 + *********************************************************************** + * According to the official specification, all strings in the torrent + * file are supposed to be UTF-8 encoded. However, there are + * non-compliant torrents around... If we encounter an invalid UTF-8 + * character, we assume it is ISO 8859-1 and convert it to UTF-8. + **********************************************************************/ +static void strcatUTF8( char * s, char * append ) +{ + char * p; + + /* Go to the end of the destination string */ + while( s[0] ) + { + s++; + } + + /* Now start appending, converting on the fly if necessary */ + for( p = append; p[0]; ) + { + if( !( p[0] & 0x80 ) ) + { + /* ASCII character */ + *(s++) = *(p++); + continue; + } + + if( ( p[0] & 0xE0 ) == 0xC0 && ( p[1] & 0xC0 ) == 0x80 ) + { + /* 2-bytes UTF-8 character */ + *(s++) = *(p++); *(s++) = *(p++); + continue; + } + + if( ( p[0] & 0xF0 ) == 0xE0 && ( p[1] & 0xC0 ) == 0x80 && + ( p[2] & 0xC0 ) == 0x80 ) + { + /* 3-bytes UTF-8 character */ + *(s++) = *(p++); *(s++) = *(p++); + *(s++) = *(p++); + continue; + } + + if( ( p[0] & 0xF8 ) == 0xF0 && ( p[1] & 0xC0 ) == 0x80 && + ( p[2] & 0xC0 ) == 0x80 && ( p[3] & 0xC0 ) == 0x80 ) + { + /* 4-bytes UTF-8 character */ + *(s++) = *(p++); *(s++) = *(p++); + *(s++) = *(p++); *(s++) = *(p++); + continue; + } + + /* ISO 8859-1 -> UTF-8 conversion */ + *(s++) = 0xC0 | ( ( *p & 0xFF ) >> 6 ); + *(s++) = 0x80 | ( *(p++) & 0x3F ); + } +} diff --git a/libtransmission/metainfo.h b/libtransmission/metainfo.h new file mode 100644 index 000000000..e0c31c9d1 --- /dev/null +++ b/libtransmission/metainfo.h @@ -0,0 +1,28 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#ifndef TR_METAINFO_H +#define TR_METAINFO_H 1 + +int tr_metainfoParse( tr_info_t *, const char * ); + +#endif diff --git a/libtransmission/net.c b/libtransmission/net.c new file mode 100644 index 000000000..7d3e9634c --- /dev/null +++ b/libtransmission/net.c @@ -0,0 +1,225 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include "transmission.h" + +static int makeSocketNonBlocking( int s ) +{ + int flags; + +#ifdef SYS_BEOS + flags = 1; + if( setsockopt( s, SOL_SOCKET, SO_NONBLOCK, + &flags, sizeof( int ) ) < 0 ) +#else + if( ( flags = fcntl( s, F_GETFL, 0 ) ) < 0 || + fcntl( s, F_SETFL, flags | O_NONBLOCK ) < 0 ) +#endif + { + tr_err( "Could not set socket to non-blocking mode (%s)", + strerror( errno ) ); + tr_netClose( s ); + return -1; + } + + return s; +} + +static int createSocket() +{ + int s; + + s = socket( AF_INET, SOCK_STREAM, 0 ); + if( s < 0 ) + { + tr_err( "Could not create socket (%s)", strerror( errno ) ); + return -1; + } + + return makeSocketNonBlocking( s ); +} + +int tr_netResolve( char * address, struct in_addr * addr ) +{ + struct hostent * host; + + addr->s_addr = inet_addr( address ); + if( addr->s_addr != 0xFFFFFFFF ) + { + return 0; + } + + if( !( host = gethostbyname( address ) ) ) + { + tr_err( "Could not resolve (%s)", address ); + return -1; + } + memcpy( addr, host->h_addr, host->h_length ); + + return 0; +} + +int tr_netOpen( struct in_addr addr, in_port_t port ) +{ + int s; + struct sockaddr_in sock; + + s = createSocket(); + if( s < 0 ) + { + return -1; + } + + memset( &sock, 0, sizeof( sock ) ); + sock.sin_family = AF_INET; + sock.sin_addr.s_addr = addr.s_addr; + sock.sin_port = port; + + if( connect( s, (struct sockaddr *) &sock, + sizeof( struct sockaddr_in ) ) < 0 && + errno != EINPROGRESS ) + { + tr_err( "Could not connect socket (%s)", strerror( errno ) ); + tr_netClose( s ); + return -1; + } + + return s; +} + +int tr_netBind( int * port ) +{ + int s, i; + struct sockaddr_in sock; + int minPort, maxPort; + + s = createSocket(); + if( s < 0 ) + { + return -1; + } + + minPort = *port; + maxPort = minPort + 1000; + maxPort = MIN( maxPort, 65535 ); + + for( i = minPort; i <= maxPort; i++ ) + { + memset( &sock, 0, sizeof( sock ) ); + sock.sin_family = AF_INET; + sock.sin_addr.s_addr = INADDR_ANY; + sock.sin_port = htons( i ); + + if( !bind( s, (struct sockaddr *) &sock, + sizeof( struct sockaddr_in ) ) ) + { + break; + } + } + + if( i > maxPort ) + { + tr_netClose( s ); + tr_err( "Could not bind any port from %d to %d", + minPort, maxPort ); + return -1; + } + + tr_inf( "Binded port %d", i ); + *port = i; + listen( s, 5 ); + + return s; +} + +int tr_netAccept( int s, struct in_addr * addr, in_port_t * port ) +{ + int t; + unsigned len; + struct sockaddr_in sock; + + len = sizeof( sock ); + t = accept( s, (struct sockaddr *) &sock, &len ); + + if( t < 0 ) + { + return -1; + } + + *addr = sock.sin_addr; + *port = sock.sin_port; + + return makeSocketNonBlocking( t ); +} + +int tr_netSend( int s, uint8_t * buf, int size ) +{ + int ret; + + ret = send( s, buf, size, 0 ); + if( ret < 0 ) + { + if( errno == ENOTCONN || errno == EAGAIN || errno == EWOULDBLOCK ) + { + ret = TR_NET_BLOCK; + } + else + { + ret = TR_NET_CLOSE; + } + } + + return ret; +} + +int tr_netRecv( int s, uint8_t * buf, int size ) +{ + int ret; + + ret = recv( s, buf, size, 0 ); + if( ret < 0 ) + { + if( errno == EAGAIN || errno == EWOULDBLOCK ) + { + ret = TR_NET_BLOCK; + } + else + { + ret = TR_NET_CLOSE; + } + } + if( !ret ) + { + ret = TR_NET_CLOSE; + } + + return ret; +} + +void tr_netClose( int s ) +{ +#ifdef BEOS_NETSERVER + closesocket( s ); +#else + close( s ); +#endif +} diff --git a/libtransmission/net.h b/libtransmission/net.h new file mode 100644 index 000000000..84548cafa --- /dev/null +++ b/libtransmission/net.h @@ -0,0 +1,32 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +int tr_netResolve ( char *, struct in_addr * ); +int tr_netOpen ( struct in_addr addr, in_port_t port ); +int tr_netBind ( int * ); +int tr_netAccept ( int s, struct in_addr *, in_port_t * ); +void tr_netClose ( int s ); + +#define TR_NET_BLOCK 0x80000000 +#define TR_NET_CLOSE 0x40000000 +int tr_netSend ( int s, uint8_t * buf, int size ); +int tr_netRecv ( int s, uint8_t * buf, int size ); diff --git a/libtransmission/peer.c b/libtransmission/peer.c new file mode 100644 index 000000000..95760f911 --- /dev/null +++ b/libtransmission/peer.c @@ -0,0 +1,464 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include "transmission.h" + +#define MAX_REQUEST_COUNT 32 +#define OUR_REQUEST_COUNT 8 /* TODO: we should detect if we are on a + high-speed network and adapt */ + +typedef struct tr_request_s +{ + int index; + int begin; + int length; + +} tr_request_t; + +struct tr_peer_s +{ + struct in_addr addr; + in_port_t port; + +#define PEER_STATUS_IDLE 1 /* Need to connect */ +#define PEER_STATUS_CONNECTING 2 /* Trying to send handshake */ +#define PEER_STATUS_HANDSHAKE 4 /* Waiting for peer's handshake */ +#define PEER_STATUS_CONNECTED 8 /* Got peer's handshake */ + int status; + int socket; + uint64_t date; + uint64_t keepAlive; + + char amChoking; + char amInterested; + char peerChoking; + char peerInterested; + + uint8_t id[20]; + + uint8_t * bitfield; + + uint8_t * buf; + int size; + int pos; + + uint8_t * outMessages; + int outMessagesSize; + int outMessagesPos; + uint8_t outBlock[13+16384]; + int outBlockSize; + int outBlockLoaded; + int outBlockSending; + + int inRequestCount; + tr_request_t inRequests[OUR_REQUEST_COUNT]; + int inIndex; + int inBegin; + int inLength; + uint64_t inTotal; + + int outRequestCount; + tr_request_t outRequests[MAX_REQUEST_COUNT]; + uint64_t outTotal; + uint64_t outDate; + int outSlow; +}; + +#define peer_dbg( a... ) __peer_dbg( peer, ## a ) +static void __peer_dbg( tr_peer_t * peer, char * msg, ... ) +{ + char string[256]; + va_list args; + + va_start( args, msg ); + sprintf( string, "%08x:%04x ", + (uint32_t) peer->addr.s_addr, peer->port ); + vsnprintf( &string[14], sizeof( string ) - 14, msg, args ); + va_end( args ); + + tr_dbg( "%s", string ); +} + +#include "peermessages.h" +#include "peerutils.h" + +/*********************************************************************** + * tr_peerAddOld + *********************************************************************** + * Tries to add a peer given its IP and port (received from a tracker + * which doesn't support the "compact" extension). + **********************************************************************/ +void tr_peerAddOld( tr_torrent_t * tor, char * ip, int port ) +{ + struct in_addr addr; + + if( tr_netResolve( ip, &addr ) ) + { + return; + } + + addWithAddr( tor, addr, htons( port ) ); +} + +/*********************************************************************** + * tr_peerAddCompact + *********************************************************************** + * Tries to add a peer. If 's' is a negative value, will use 'addr' and + * 'port' to connect to the peer. Otherwise, use the already connected + * socket 's'. + **********************************************************************/ +void tr_peerAddCompact( tr_torrent_t * tor, struct in_addr addr, + in_port_t port, int s ) +{ + tr_peer_t * peer; + + if( s < 0 ) + { + addWithAddr( tor, addr, port ); + return; + } + + if( !( peer = peerInit( tor ) ) ) + { + tr_netClose( s ); + tr_fdSocketClosed( tor->fdlimit, 0 ); + return; + } + + peer->socket = s; + peer->addr = addr; + peer->port = port; + peer->status = PEER_STATUS_CONNECTING; +} + +/*********************************************************************** + * tr_peerRem + *********************************************************************** + * Frees and closes everything related to the peer at index 'i', and + * removes it from the peers list. + **********************************************************************/ +void tr_peerRem( tr_torrent_t * tor, int i ) +{ + tr_peer_t * peer = tor->peers[i]; + int j; + + for( j = 0; j < peer->inRequestCount; j++ ) + { + tr_request_t * r; + int block; + + r = &peer->inRequests[j]; + block = tr_block( r->index,r->begin ); + if( tor->blockHave[block] > 0 ) + { + (tor->blockHave[block])--; + } + } + if( !peer->amChoking ) + { + tr_uploadChoked( tor->upload ); + } + if( peer->bitfield ) + { + free( peer->bitfield ); + } + if( peer->buf ) + { + free( peer->buf ); + } + if( peer->outMessages ) + { + free( peer->outMessages ); + } + if( peer->status > PEER_STATUS_IDLE ) + { + tr_netClose( peer->socket ); + tr_fdSocketClosed( tor->fdlimit, 0 ); + } + free( peer ); + tor->peerCount--; + memmove( &tor->peers[i], &tor->peers[i+1], + ( tor->peerCount - i ) * sizeof( tr_peer_t * ) ); +} + +/*********************************************************************** + * tr_peerPulse + *********************************************************************** + * + **********************************************************************/ +void tr_peerPulse( tr_torrent_t * tor ) +{ + int i, ret, size; + uint8_t * p; + tr_peer_t * peer; + + tor->dates[9] = tr_date(); + if( tor->dates[9] > tor->dates[8] + 1000 ) + { + memmove( &tor->downloaded[0], &tor->downloaded[1], + 9 * sizeof( uint64_t ) ); + memmove( &tor->uploaded[0], &tor->uploaded[1], + 9 * sizeof( uint64_t ) ); + memmove( &tor->dates[0], &tor->dates[1], + 9 * sizeof( uint64_t ) ); + + for( i = 0; i < tor->peerCount; ) + { + if( checkPeer( tor, i ) ) + { + tr_peerRem( tor, i ); + continue; + } + i++; + } + } + + /* Check for incoming connections */ + if( tor->bindSocket > -1 && + tor->peerCount < TR_MAX_PEER_COUNT && + !tr_fdSocketWillCreate( tor->fdlimit, 0 ) ) + { + int s; + struct in_addr addr; + in_port_t port; + s = tr_netAccept( tor->bindSocket, &addr, &port ); + if( s > -1 ) + { + tr_peerAddCompact( tor, addr, port, s ); + } + else + { + tr_fdSocketClosed( tor->fdlimit, 0 ); + } + } + + /* Shuffle peers */ + if( tor->peerCount > 1 ) + { + peer = tor->peers[0]; + memmove( &tor->peers[0], &tor->peers[1], + ( tor->peerCount - 1 ) * sizeof( void * ) ); + tor->peers[tor->peerCount - 1] = peer; + } + + /* Handle peers */ + for( i = 0; i < tor->peerCount; ) + { + peer = tor->peers[i]; + + /* Connect */ + if( ( peer->status & PEER_STATUS_IDLE ) && + !tr_fdSocketWillCreate( tor->fdlimit, 0 ) ) + { + peer->socket = tr_netOpen( peer->addr, peer->port ); + if( peer->socket < 0 ) + { + peer_dbg( "connection failed" ); + goto dropPeer; + } + peer->status = PEER_STATUS_CONNECTING; + } + + /* Try to send handshake */ + if( peer->status & PEER_STATUS_CONNECTING ) + { + uint8_t buf[68]; + tr_info_t * inf = &tor->info; + + buf[0] = 19; + memcpy( &buf[1], "BitTorrent protocol", 19 ); + memset( &buf[20], 0, 8 ); + memcpy( &buf[28], inf->hash, 20 ); + memcpy( &buf[48], tor->id, 20 ); + + ret = tr_netSend( peer->socket, buf, 68 ); + if( ret & TR_NET_CLOSE ) + { + peer_dbg( "connection closed" ); + goto dropPeer; + } + else if( !( ret & TR_NET_BLOCK ) ) + { + peer_dbg( "SEND handshake" ); + peer->status = PEER_STATUS_HANDSHAKE; + } + } + + /* Try to read */ + if( peer->status >= PEER_STATUS_HANDSHAKE ) + { + for( ;; ) + { + if( peer->size < 1 ) + { + peer->size = 1024; + peer->buf = malloc( peer->size ); + } + else if( peer->pos >= peer->size ) + { + peer->size *= 2; + peer->buf = realloc( peer->buf, peer->size ); + } + ret = tr_netRecv( peer->socket, &peer->buf[peer->pos], + peer->size - peer->pos ); + if( ret & TR_NET_CLOSE ) + { + peer_dbg( "connection closed" ); + goto dropPeer; + } + else if( ret & TR_NET_BLOCK ) + { + break; + } + peer->date = tr_date(); + peer->pos += ret; + if( parseMessage( tor, peer, ret ) ) + { + goto dropPeer; + } + } + } + + /* Try to write */ +writeBegin: + + /* Send all smaller messages regardless of the upload cap */ + while( ( p = messagesPending( peer, &size ) ) ) + { + ret = tr_netSend( peer->socket, p, size ); + if( ret & TR_NET_CLOSE ) + { + goto dropPeer; + } + else if( ret & TR_NET_BLOCK ) + { + goto writeEnd; + } + messagesSent( peer, ret ); + } + + /* Send pieces if we can */ + while( ( p = blockPending( tor, peer, &size ) ) ) + { + if( !tr_uploadCanUpload( tor->upload ) ) + { + break; + } + + ret = tr_netSend( peer->socket, p, size ); + if( ret & TR_NET_CLOSE ) + { + goto dropPeer; + } + else if( ret & TR_NET_BLOCK ) + { + break; + } + + blockSent( peer, ret ); + tr_uploadUploaded( tor->upload, ret ); + + tor->uploaded[9] += ret; + peer->outTotal += ret; + peer->outDate = tr_date(); + + /* In case this block is done, you may have messages + pending. Send them before we start the next block */ + goto writeBegin; + } +writeEnd: + + /* Connected peers: ask for a block whenever possible */ + if( peer->status & PEER_STATUS_CONNECTED ) + { + if( tor->blockHaveCount < tor->blockCount && + !peer->amInterested && tor->peerCount > TR_MAX_PEER_COUNT - 2 ) + { + /* This peer is no use to us, and it seems there are + more */ + peer_dbg( "not interesting" ); + tr_peerRem( tor, i ); + continue; + } + + if( peer->amInterested && !peer->peerChoking ) + { + int block; + while( peer->inRequestCount < OUR_REQUEST_COUNT ) + { + block = chooseBlock( tor, peer ); + if( block < 0 ) + { + break; + } + sendRequest( tor, peer, block ); + } + } + } + + i++; + continue; + +dropPeer: + tr_peerRem( tor, i ); + } +} + +/*********************************************************************** + * tr_peerIsConnected + *********************************************************************** + * + **********************************************************************/ +int tr_peerIsConnected( tr_peer_t * peer ) +{ + return peer->status & PEER_STATUS_CONNECTED; +} + +/*********************************************************************** + * tr_peerIsUploading + *********************************************************************** + * + **********************************************************************/ +int tr_peerIsUploading( tr_peer_t * peer ) +{ + return ( peer->inRequestCount > 0 ); +} + +/*********************************************************************** + * tr_peerIsDownloading + *********************************************************************** + * + **********************************************************************/ +int tr_peerIsDownloading( tr_peer_t * peer ) +{ + return peer->outBlockSending; +} + +/*********************************************************************** + * tr_peerBitfield + *********************************************************************** + * + **********************************************************************/ +uint8_t * tr_peerBitfield( tr_peer_t * peer ) +{ + return peer->bitfield; +} diff --git a/libtransmission/peer.h b/libtransmission/peer.h new file mode 100644 index 000000000..bbbae6bd2 --- /dev/null +++ b/libtransmission/peer.h @@ -0,0 +1,38 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#ifndef TR_PEER_H +#define TR_PEER_H 1 + +typedef struct tr_peer_s tr_peer_t; + +void tr_peerAddOld ( tr_torrent_t *, char *, int ); +void tr_peerAddCompact ( tr_torrent_t *, struct in_addr, + in_port_t, int ); +void tr_peerRem ( tr_torrent_t *, int ); +void tr_peerPulse ( tr_torrent_t * ); +int tr_peerIsConnected ( tr_peer_t * ); +int tr_peerIsUploading ( tr_peer_t * ); +int tr_peerIsDownloading ( tr_peer_t * ); +uint8_t * tr_peerBitfield ( tr_peer_t * ); + +#endif diff --git a/libtransmission/peermessages.h b/libtransmission/peermessages.h new file mode 100644 index 000000000..d740a36c0 --- /dev/null +++ b/libtransmission/peermessages.h @@ -0,0 +1,306 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +static uint8_t * messagesPending( tr_peer_t * peer, int * size ) +{ + if( peer->outBlockSending || peer->outMessagesPos < 1 ) + { + return NULL; + } + + *size = MIN( peer->outMessagesPos, 1024 ); + + return peer->outMessages; +} + +static void messagesSent( tr_peer_t * peer, int size ) +{ + peer->outMessagesPos -= size; + memmove( peer->outMessages, &peer->outMessages[size], + peer->outMessagesPos ); +} + +static uint8_t * blockPending( tr_torrent_t * tor, tr_peer_t * peer, + int * size ) +{ + if( !peer->outBlockLoaded ) + { + uint8_t * p; + tr_request_t * r; + + if( peer->outRequestCount < 1 ) + { + /* No piece to send */ + return NULL; + } + + /* We need to load the block for the next request */ + r = &peer->outRequests[0]; + p = (uint8_t *) peer->outBlock; + + TR_HTONL( 9 + r->length, p ); + p[4] = 7; + TR_HTONL( r->index, p + 5 ); + TR_HTONL( r->begin, p + 9 ); + + tr_ioRead( tor->io, r->index, r->begin, r->length, &p[13] ); + + peer_dbg( "SEND piece %d/%d (%d bytes)", + r->index, r->begin, r->length ); + + peer->outBlockSize = 13 + r->length; + peer->outBlockLoaded = 1; + + (peer->outRequestCount)--; + memmove( &peer->outRequests[0], &peer->outRequests[1], + peer->outRequestCount * sizeof( tr_request_t ) ); + } + + *size = MIN( 1024, peer->outBlockSize ); + + return (uint8_t *) peer->outBlock; +} + +static void blockSent( tr_peer_t * peer, int size ) +{ + peer->outBlockSize -= size; + memmove( peer->outBlock, &peer->outBlock[size], peer->outBlockSize ); + + if( peer->outBlockSize > 0 ) + { + /* We can't send messages until we are done sending the block */ + peer->outBlockSending = 1; + } + else + { + /* Block fully sent */ + peer->outBlockSending = 0; + peer->outBlockLoaded = 0; + } +} + +static uint8_t * getPointerForSize( tr_peer_t * peer, int size ) +{ + uint8_t * p; + + if( peer->outMessagesPos + size > peer->outMessagesSize ) + { + peer->outMessagesSize = peer->outMessagesPos + size; + peer->outMessages = realloc( peer->outMessages, + peer->outMessagesSize ); + } + + p = &peer->outMessages[peer->outMessagesPos]; + peer->outMessagesPos += size; + + return p; +} + +/*********************************************************************** + * sendKeepAlive + *********************************************************************** + * + **********************************************************************/ +static void sendKeepAlive( tr_peer_t * peer ) +{ + uint8_t * p; + + p = getPointerForSize( peer, 4 ); + + TR_HTONL( 0, p ); + + peer_dbg( "SEND keep-alive" ); +} + + +/*********************************************************************** + * sendChoke + *********************************************************************** + * + **********************************************************************/ +static void sendChoke( tr_peer_t * peer, int yes ) +{ + uint8_t * p; + + p = getPointerForSize( peer, 5 ); + + TR_HTONL( 1, p ); + p[4] = yes ? 0 : 1; + + peer->amChoking = yes; + + if( yes ) + { + /* Drop all pending requests */ + peer->outRequestCount = 0; + } + + peer_dbg( "SEND %schoke", yes ? "" : "un" ); +} + +/*********************************************************************** + * sendInterest + *********************************************************************** + * + **********************************************************************/ +static void sendInterest( tr_peer_t * peer, int yes ) +{ + uint8_t * p; + + p = getPointerForSize( peer, 5 ); + + TR_HTONL( 1, p ); + p[4] = yes ? 2 : 3; + + peer->amInterested = yes; + + peer_dbg( "SEND %sinterested", yes ? "" : "un" ); +} + +/*********************************************************************** + * sendHave + *********************************************************************** + * + **********************************************************************/ +static void sendHave( tr_peer_t * peer, int piece ) +{ + uint8_t * p; + + p = getPointerForSize( peer, 9 ); + + TR_HTONL( 5, &p[0] ); + p[4] = 4; + TR_HTONL( piece, &p[5] ); + + peer_dbg( "SEND have %d", piece ); +} + +/*********************************************************************** + * sendBitfield + *********************************************************************** + * Builds a 'bitfield' message: + * - size = 5 + X (4 bytes) + * - id = 5 (1 byte) + * - bitfield (X bytes) + **********************************************************************/ +static void sendBitfield( tr_torrent_t * tor, tr_peer_t * peer ) +{ + uint8_t * p; + int bitfieldSize = ( tor->info.pieceCount + 7 ) / 8; + + p = getPointerForSize( peer, 5 + bitfieldSize ); + + TR_HTONL( 1 + bitfieldSize, p ); + p[4] = 5; + memcpy( &p[5], tor->bitfield, bitfieldSize ); + + peer_dbg( "SEND bitfield" ); +} + +/*********************************************************************** + * sendRequest + *********************************************************************** + * + **********************************************************************/ +static void sendRequest( tr_torrent_t * tor, tr_peer_t * peer, int block ) +{ + tr_info_t * inf = &tor->info; + tr_request_t * r; + uint8_t * p; + + /* Get the piece the block is a part of, its position in the piece + and its size */ + r = &peer->inRequests[peer->inRequestCount]; + r->index = block / ( inf->pieceSize / tor->blockSize ); + r->begin = ( block % ( inf->pieceSize / tor->blockSize ) ) * + tor->blockSize; + r->length = tor->blockSize; + if( block == tor->blockCount - 1 ) + { + int lastSize = inf->totalSize % tor->blockSize; + if( lastSize ) + { + r->length = lastSize; + } + } + (peer->inRequestCount)++; + + /* Build the "ask" message */ + p = getPointerForSize( peer, 17 ); + + TR_HTONL( 13, p ); + p[4] = 6; + TR_HTONL( r->index, p + 5 ); + TR_HTONL( r->begin, p + 9 ); + TR_HTONL( r->length, p + 13 ); + + /* Remember that we have one more uploader for this block */ + (tor->blockHave[block])++; + + peer_dbg( "SEND request %d/%d (%d bytes)", + r->index, r->begin, r->length ); +} + +/*********************************************************************** + * sendCancel + *********************************************************************** + * + **********************************************************************/ +static void sendCancel( tr_torrent_t * tor, int block ) +{ + int i, j; + uint8_t * p; + tr_peer_t * peer; + tr_request_t * r; + + for( i = 0; i < tor->peerCount; i++ ) + { + peer = tor->peers[i]; + + for( j = 1; j < peer->inRequestCount; j++ ) + { + r = &peer->inRequests[j]; + + if( block != tr_block( r->index, r->begin ) ) + { + continue; + } + + p = getPointerForSize( peer, 17 ); + + /* Build the "cancel" message */ + TR_HTONL( 13, p ); + p[4] = 8; + TR_HTONL( r->index, p + 5 ); + TR_HTONL( r->begin, p + 9 ); + TR_HTONL( r->length, p + 13 ); + + peer_dbg( "SEND cancel %d/%d (%d bytes)", + r->index, r->begin, r->length ); + + (peer->inRequestCount)--; + memmove( &peer->inRequests[j], &peer->inRequests[j+1], + ( peer->inRequestCount - j ) * sizeof( tr_request_t ) ); + break; + } + } +} diff --git a/libtransmission/peerutils.h b/libtransmission/peerutils.h new file mode 100644 index 000000000..d7319214c --- /dev/null +++ b/libtransmission/peerutils.h @@ -0,0 +1,827 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +static void updateInterest( tr_torrent_t * tor, tr_peer_t * peer ); + +/*********************************************************************** + * peerInit + *********************************************************************** + * Returns NULL if we reached the maximum authorized number of peers. + * Otherwise, allocates a new tr_peer_t, add it to the peers list and + * returns a pointer to it. + **********************************************************************/ +static tr_peer_t * peerInit( tr_torrent_t * tor ) +{ + tr_peer_t * peer; + + if( tor->peerCount >= TR_MAX_PEER_COUNT ) + { + return NULL; + } + + peer = calloc( sizeof( tr_peer_t ), 1 ); + peer->amChoking = 1; + peer->peerChoking = 1; + peer->date = tr_date(); + peer->keepAlive = peer->date; + + tor->peers[tor->peerCount++] = peer; + return peer; +} + +static int peerCmp( tr_peer_t * peer1, tr_peer_t * peer2 ) +{ + /* Wait until we got the peers' ids */ + if( peer1->status < PEER_STATUS_CONNECTED || + peer2->status < PEER_STATUS_CONNECTED ) + { + return 1; + } + + return memcmp( peer1->id, peer2->id, 20 ); +} + +/*********************************************************************** + * addWithAddr + *********************************************************************** + * Does nothing if we already have a peer matching 'addr' and 'port'. + * Otherwise adds such a new peer. + **********************************************************************/ +static void addWithAddr( tr_torrent_t * tor, struct in_addr addr, + in_port_t port ) +{ + int i; + tr_peer_t * peer; + + for( i = 0; i < tor->peerCount; i++ ) + { + peer = tor->peers[i]; + if( peer->addr.s_addr == addr.s_addr && + peer->port == port ) + { + /* We are already connected to this peer */ + return; + } + } + + if( !( peer = peerInit( tor ) ) ) + { + return; + } + + peer->addr = addr; + peer->port = port; + peer->status = PEER_STATUS_IDLE; +} + +static int checkPeer( tr_torrent_t * tor, int i ) +{ + tr_peer_t * peer = tor->peers[i]; + + if( peer->status < PEER_STATUS_CONNECTED && + tr_date() > peer->date + 8000 ) + { + /* If it has been too long, don't wait for the socket + to timeout - forget about it now */ + peer_dbg( "connection timeout" ); + return 1; + } + + /* Drop peers who haven't even sent a keep-alive within the + last 3 minutes */ + if( tr_date() > peer->date + 180000 ) + { + peer_dbg( "read timeout" ); + return 1; + } + + /* Drop peers which are supposed to upload but actually + haven't sent anything within the last minute */ + if( peer->inRequestCount && tr_date() > peer->date + 60000 ) + { + peer_dbg( "bad uploader" ); + return 1; + } + +#if 0 + /* Choke unchoked peers we are not sending anything to */ + if( !peer->amChoking && tr_date() > peer->outDate + 10000 ) + { + peer_dbg( "not worth the unchoke" ); + if( sendChoke( peer, 1 ) ) + { + goto dropPeer; + } + peer->outSlow = 1; + tr_uploadChoked( tor->upload ); + } +#endif + + if( peer->status & PEER_STATUS_CONNECTED ) + { + /* Send keep-alive every 2 minutes */ + if( tr_date() > peer->keepAlive + 120000 ) + { + sendKeepAlive( peer ); + peer->keepAlive = tr_date(); + } + + /* Choke or unchoke some people */ + /* TODO: prefer people who upload to us */ + if( !peer->amChoking && !peer->peerInterested ) + { + /* He doesn't need us */ + sendChoke( peer, 1 ); + tr_uploadChoked( tor->upload ); + } + if( peer->amChoking && peer->peerInterested && + !peer->outSlow && tr_uploadCanUnchoke( tor->upload ) ) + { + sendChoke( peer, 0 ); + tr_uploadUnchoked( tor->upload ); + } + } + + return 0; +} + +static int parseMessage( tr_torrent_t * tor, tr_peer_t * peer, + int newBytes ) +{ + tr_info_t * inf = &tor->info; + + int i, j; + int len; + char id; + uint8_t * p = peer->buf; + uint8_t * end = &p[peer->pos]; + + for( ;; ) + { + if( peer->pos < 4 ) + { + break; + } + + if( peer->status & PEER_STATUS_HANDSHAKE ) + { + char * client; + + if( p[0] != 19 || memcmp( &p[1], "Bit", 3 ) ) + { + /* Don't wait until we get 68 bytes, this is wrong + already */ + peer_dbg( "GET handshake, invalid" ); + tr_netSend( peer->socket, (uint8_t *) "Nice try...\r\n", 13 ); + return 1; + } + + if( peer->pos < 68 ) + { + break; + } + + if( memcmp( &p[4], "Torrent protocol", 16 ) ) + { + peer_dbg( "GET handshake, invalid" ); + return 1; + } + + if( memcmp( &p[28], inf->hash, 20 ) ) + { + peer_dbg( "GET handshake, wrong torrent hash" ); + return 1; + } + + if( !memcmp( &p[48], tor->id, 20 ) ) + { + /* We are connected to ourselves... */ + peer_dbg( "GET handshake, that is us" ); + return 1; + } + + peer->status = PEER_STATUS_CONNECTED; + memcpy( peer->id, &p[48], 20 ); + p += 68; + peer->pos -= 68; + + for( i = 0; i < tor->peerCount; i++ ) + { + if( tor->peers[i] == peer ) + { + continue; + } + if( !peerCmp( peer, tor->peers[i] ) ) + { + peer_dbg( "GET handshake, duplicate" ); + return 1; + } + } + + client = tr_clientForId( (uint8_t *) peer->id ); + peer_dbg( "GET handshake, ok (%s)", client ); + free( client ); + + sendBitfield( tor, peer ); + + continue; + } + + /* Get payload size */ + TR_NTOHL( p, len ); + p += 4; + + if( len > 9 + tor->blockSize ) + { + /* This shouldn't happen. Forget about that peer */ + peer_dbg( "message too large" ); + return 1; + } + + if( !len ) + { + /* keep-alive */ + peer_dbg( "GET keep-alive" ); + peer->pos -= 4; + continue; + } + + /* That's a piece coming */ + if( p < end && *p == 7 ) + { + /* XXX */ + tor->downloaded[9] += newBytes; + peer->inTotal += newBytes; + newBytes = 0; + } + + if( &p[len] > end ) + { + /* We do not have the entire message */ + p -= 4; + break; + } + + /* Remaining data after this message */ + peer->pos -= 4 + len; + + /* Type of the message */ + id = *(p++); + + switch( id ) + { + case 0: /* choke */ + { + tr_request_t * r; + + if( len != 1 ) + { + peer_dbg( "GET choke, invalid" ); + return 1; + } + + peer_dbg( "GET choke" ); + peer->peerChoking = 1; + + for( i = 0; i < peer->inRequestCount; i++ ) + { + r = &peer->inRequests[i]; + if( tor->blockHave[tr_block(r->index,r->begin)] > 0 ) + { + tor->blockHave[tr_block(r->index,r->begin)]--; + } + } + peer->inRequestCount = 0; + + break; + } + case 1: /* unchoke */ + if( len != 1 ) + { + peer_dbg( "GET unchoke, invalid" ); + return 1; + } + peer_dbg( "GET unchoke" ); + peer->peerChoking = 0; + break; + case 2: /* interested */ + if( len != 1 ) + { + peer_dbg( "GET interested, invalid" ); + return 1; + } + peer_dbg( "GET interested" ); + peer->peerInterested = 1; + break; + case 3: /* uninterested */ + if( len != 1 ) + { + peer_dbg( "GET uninterested, invalid" ); + return 1; + } + peer_dbg( "GET uninterested" ); + peer->peerInterested = 0; + break; + case 4: /* have */ + { + uint32_t piece; + if( len != 5 ) + { + peer_dbg( "GET have, invalid" ); + return 1; + } + TR_NTOHL( p, piece ); + if( !peer->bitfield ) + { + peer->bitfield = calloc( ( inf->pieceCount + 7 ) / 8, 1 ); + } + tr_bitfieldAdd( peer->bitfield, piece ); + + updateInterest( tor, peer ); + + peer_dbg( "GET have %d", piece ); + break; + } + case 5: /* bitfield */ + { + int bitfieldSize; + + bitfieldSize = ( inf->pieceCount + 7 ) / 8; + + if( len != 1 + bitfieldSize ) + { + peer_dbg( "GET bitfield, wrong size" ); + return 1; + } + + /* Make sure the spare bits are unset */ + if( ( inf->pieceCount & 0x7 ) ) + { + uint8_t lastByte; + + lastByte = p[bitfieldSize-1]; + lastByte <<= inf->pieceCount & 0x7; + lastByte &= 0xFF; + + if( lastByte ) + { + peer_dbg( "GET bitfield, spare bits set" ); + return 1; + } + } + + if( !peer->bitfield ) + { + peer->bitfield = malloc( bitfieldSize ); + } + memcpy( peer->bitfield, p, bitfieldSize ); + + updateInterest( tor, peer ); + + peer_dbg( "GET bitfield, ok" ); + break; + } + case 6: /* request */ + { + int index, begin, length; + + if( peer->amChoking ) + { + /* Didn't he get it? */ + sendChoke( peer, 1 ); + break; + } + + TR_NTOHL( p, index ); + TR_NTOHL( &p[4], begin ); + TR_NTOHL( &p[8], length ); + + peer_dbg( "GET request %d/%d (%d bytes)", + index, begin, length ); + + /* TODO sanity checks (do we have the piece, etc) */ + + if( length > 16384 ) + { + /* Sorry mate */ + return 1; + } + + if( peer->outRequestCount < MAX_REQUEST_COUNT ) + { + tr_request_t * r; + + r = &peer->outRequests[peer->outRequestCount]; + r->index = index; + r->begin = begin; + r->length = length; + + (peer->outRequestCount)++; + } + else + { + tr_err( "Too many requests" ); + return 1; + } + break; + } + case 7: /* piece */ + { + int index, begin; + int block; + tr_request_t * r; + + TR_NTOHL( p, index ); + TR_NTOHL( &p[4], begin ); + + peer_dbg( "GET piece %d/%d (%d bytes)", + index, begin, len - 9 ); + + if( peer->inRequestCount < 1 ) + { + /* Our "cancel" was probably late */ + peer_dbg( "not expecting a block" ); + break; + } + + r = &peer->inRequests[0]; + if( index != r->index || begin != r->begin ) + { + int suckyClient; + + /* Either our "cancel" was late, or this is a sucky + client that cannot deal with multiple requests */ + suckyClient = 0; + for( i = 0; i < peer->inRequestCount; i++ ) + { + r = &peer->inRequests[i]; + + if( index != r->index || begin != r->begin ) + { + continue; + } + + /* Sucky client, he dropped the previous requests */ + peer_dbg( "block was expected later" ); + for( j = 0; j < i; j++ ) + { + r = &peer->inRequests[j]; + if( tor->blockHave[tr_block(r->index,r->begin)] > 0 ) + { + tor->blockHave[tr_block(r->index,r->begin)]--; + } + } + suckyClient = 1; + peer->inRequestCount -= i; + memmove( &peer->inRequests[0], &peer->inRequests[i], + peer->inRequestCount * sizeof( tr_request_t ) ); + r = &peer->inRequests[0]; + break; + } + + if( !suckyClient ) + { + r = &peer->inRequests[0]; + peer_dbg( "wrong block (expecting %d/%d)", + r->index, r->begin ); + break; + } + } + + if( len - 9 != r->length ) + { + peer_dbg( "wrong size (expecting %d)", r->length ); + return 1; + } + + block = tr_block( r->index, r->begin ); + if( tor->blockHave[block] < 0 ) + { + peer_dbg( "have this block already" ); + (peer->inRequestCount)--; + memmove( &peer->inRequests[0], &peer->inRequests[1], + peer->inRequestCount * sizeof( tr_request_t ) ); + break; + } + + tor->blockHave[block] = -1; + tor->blockHaveCount += 1; + tr_ioWrite( tor->io, index, begin, len - 9, &p[8] ); + + sendCancel( tor, block ); + + if( tr_bitfieldHas( tor->bitfield, index ) ) + { + tr_peer_t * otherPeer; + + for( i = 0; i < tor->peerCount; i++ ) + { + otherPeer = tor->peers[i]; + + if( otherPeer->status < PEER_STATUS_CONNECTED ) + { + continue; + } + + sendHave( otherPeer, index ); + updateInterest( tor, otherPeer ); + } + } + + (peer->inRequestCount)--; + memmove( &peer->inRequests[0], &peer->inRequests[1], + peer->inRequestCount * sizeof( tr_request_t ) ); + break; + } + case 8: /* cancel */ + { + int index, begin, length; + int i; + tr_request_t * r; + + TR_NTOHL( p, index ); + TR_NTOHL( &p[4], begin ); + TR_NTOHL( &p[8], length ); + + peer_dbg( "GET cancel %d/%d (%d bytes)", + index, begin, length ); + + for( i = 0; i < peer->outRequestCount; i++ ) + { + r = &peer->outRequests[i]; + if( r->index == index && r->begin == begin && + r->length == length ) + { + (peer->outRequestCount)--; + memmove( &r[0], &r[1], sizeof( tr_request_t ) * + ( peer->outRequestCount - i ) ); + break; + } + } + + break; + } + case 9: + { + in_port_t port; + + if( len != 3 ) + { + peer_dbg( "GET port, invalid" ); + return 1; + } + + port = *( (in_port_t *) p ); + peer_dbg( "GET port %d", ntohs( port ) ); + + break; + } + default: + { + peer_dbg( "Unknown message '%d'", id ); + return 1; + } + } + + p += len - 1; + } + + memmove( peer->buf, p, peer->pos ); + + return 0; +} + +/*********************************************************************** + * isInteresting + *********************************************************************** + * Returns 1 if 'peer' has at least one piece that we haven't completed, + * or 0 otherwise. + **********************************************************************/ +static int isInteresting( tr_torrent_t * tor, tr_peer_t * peer ) +{ + tr_info_t * inf = &tor->info; + + int i; + int bitfieldSize = ( inf->pieceCount + 7 ) / 8; + + if( !peer->bitfield ) + { + /* We don't know what this peer has */ + return 0; + } + + for( i = 0; i < bitfieldSize; i++ ) + { + if( ( peer->bitfield[i] & ~(tor->bitfield[i]) ) & 0xFF ) + { + return 1; + } + } + + return 0; +} +static void updateInterest( tr_torrent_t * tor, tr_peer_t * peer ) +{ + int interested = isInteresting( tor, peer ); + + if( interested && !peer->amInterested ) + { + sendInterest( peer, 1 ); + } + if( !interested && peer->amInterested ) + { + sendInterest( peer, 0 ); + } +} + +/*********************************************************************** + * chooseBlock + *********************************************************************** + * At this point, we know the peer has at least one block we have an + * interest in. If he has more than one, we choose which one we are + * going to ask first. + * Our main goal is to complete pieces, so we look the pieces which are + * missing less blocks. + **********************************************************************/ +static int chooseBlock( tr_torrent_t * tor, tr_peer_t * peer ) +{ + tr_info_t * inf = &tor->info; + + int i, j; + int startBlock, endBlock, countBlocks; + int missingBlocks, minMissing; + int poolSize, * pool; + int block, minDownloading; + + /* Choose a piece */ + pool = malloc( inf->pieceCount * sizeof( int ) ); + poolSize = 0; + minMissing = tor->blockCount + 1; + for( i = 0; i < inf->pieceCount; i++ ) + { + if( !tr_bitfieldHas( peer->bitfield, i ) ) + { + /* The peer doesn't have this piece */ + continue; + } + if( tr_bitfieldHas( tor->bitfield, i ) ) + { + /* We already have it */ + continue; + } + + /* Count how many blocks from this piece are missing */ + startBlock = tr_pieceStartBlock( i ); + countBlocks = tr_pieceCountBlocks( i ); + endBlock = startBlock + countBlocks; + missingBlocks = countBlocks; + for( j = startBlock; j < endBlock; j++ ) + { + /* TODO: optimize */ + if( tor->blockHave[j] ) + { + missingBlocks--; + } + if( missingBlocks > minMissing ) + { + break; + } + } + + if( missingBlocks < 1 ) + { + /* We are already downloading all blocks */ + continue; + } + + /* We are interested in this piece, remember it */ + if( missingBlocks < minMissing ) + { + minMissing = missingBlocks; + poolSize = 0; + } + if( missingBlocks <= minMissing ) + { + pool[poolSize++] = i; + } + } + + if( poolSize ) + { + /* All pieces in 'pool' have 'minMissing' missing blocks. Find + the rarest ones. */ + uint8_t * bitfield; + int piece; + int min, foo, j; + int * pool2; + int pool2Size; + + pool2 = malloc( poolSize * sizeof( int ) ); + pool2Size = 0; + min = TR_MAX_PEER_COUNT + 1; + for( i = 0; i < poolSize; i++ ) + { + foo = 0; + for( j = 0; j < tor->peerCount; j++ ) + { + bitfield = tor->peers[j]->bitfield; + if( bitfield && tr_bitfieldHas( bitfield, pool[i] ) ) + { + foo++; + } + } + if( foo < min ) + { + min = foo; + pool2Size = 0; + } + if( foo <= min ) + { + pool2[pool2Size++] = pool[i]; + } + } + free( pool ); + + if( pool2Size < 1 ) + { + /* Shouldn't happen */ + free( pool2 ); + return -1; + } + + /* All pieces in pool2 have the same number of missing blocks, + and are availabme from the same number of peers. Pick a + random one */ + piece = pool2[tr_rand(pool2Size)]; + free( pool2 ); + + /* Pick a block in this piece */ + startBlock = tr_pieceStartBlock( piece ); + endBlock = startBlock + tr_pieceCountBlocks( piece ); + for( i = startBlock; i < endBlock; i++ ) + { + if( !tor->blockHave[i] ) + { + block = i; + goto check; + } + } + + /* Shouldn't happen */ + return -1; + } + + free( pool ); + + /* "End game" mode */ + block = -1; + minDownloading = TR_MAX_PEER_COUNT + 1; + for( i = 0; i < tor->blockCount; i++ ) + { + /* TODO: optimize */ + if( tor->blockHave[i] > 0 && tor->blockHave[i] < minDownloading ) + { + block = i; + minDownloading = tor->blockHave[i]; + } + } + + if( block < 0 ) + { + /* Shouldn't happen */ + return -1; + } + +check: + for( i = 0; i < peer->inRequestCount; i++ ) + { + tr_request_t * r; + r = &peer->inRequests[i]; + if( tr_block( r->index, r->begin ) == block ) + { + /* We are already asking this peer for this block */ + return -1; + } + } + + return block; +} diff --git a/libtransmission/sha1.c b/libtransmission/sha1.c new file mode 100644 index 000000000..d9d3b37bf --- /dev/null +++ b/libtransmission/sha1.c @@ -0,0 +1,235 @@ +/* + sha1.c: Implementation of SHA-1 Secure Hash Algorithm-1 + + Based upon: NIST FIPS180-1 Secure Hash Algorithm-1 + http://www.itl.nist.gov/fipspubs/fip180-1.htm + + Non-official Japanese Translation by HIRATA Yasuyuki: + http://yasu.asuka.net/translations/SHA-1.html + + Copyright (C) 2002 vi@nwr.jp. All rights reserved. + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgement in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as beging the original software. + 3. This notice may not be removed or altered from any source distribution. + + Note: + The copyright notice above is copied from md5.h by L. Peter Deutsch + . Thank him since I'm not a good speaker of English. :) + */ +#include +#include "sha1.h" + +#define INLINE inline +/* + * Packing bytes to a word + * + * Should not assume p is aligned to word boundary + */ +static INLINE sha1_word_t packup(sha1_byte_t *p) +{ + /* Portable, but slow */ + return p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3] << 0; +} + +/* + * Unpacking a word to bytes + * + * Should not assume p is aligned to word boundary + */ +static void unpackup(sha1_byte_t *p, sha1_word_t q) +{ + p[0] = (q >> 24) & 0xff; + p[1] = (q >> 16) & 0xff; + p[2] = (q >> 8) & 0xff; + p[3] = (q >> 0) & 0xff; +} + +/* + * Processing a block + */ +static inline void sha1_update_now(sha1_state_s *pms, sha1_byte_t *bp) +{ + sha1_word_t tmp, a, b, c, d, e, w[16+16]; + int i, s; + + /* pack 64 bytes into 16 words */ + for(i = 0; i < 16; i++) { + w[i] = packup(bp + i * sizeof(sha1_word_t)); + } + memcpy(w + 16, w + 0, sizeof(sha1_word_t) * 16); + + a = pms->sha1_h[0], b = pms->sha1_h[1], c = pms->sha1_h[2], d = pms->sha1_h[3], e = pms->sha1_h[4]; + +#define rot(x,n) (((x) << n) | ((x) >> (32-n))) +#define f0(b, c, d) ((b&c)|(~b&d)) +#define f1(b, c, d) (b^c^d) +#define f2(b, c, d) ((b&c)|(b&d)|(c&d)) +#define f3(b, c, d) (b^c^d) +#define k0 0x5a827999 +#define k1 0x6ed9eba1 +#define k2 0x8f1bbcdc +#define k3 0xca62c1d6 + + /* t=0-15 */ + s = 0; + for(i = 0; i < 16; i++) { + tmp = rot(a, 5) + f0(b, c, d) + e + w[s] + k0; + e = d; d = c; c = rot(b, 30); b = a; a = tmp; + s = (s + 1) % 16; + } + + /* t=16-19 */ + for(i = 16; i < 20; i++) { + w[s] = rot(w[s+13] ^ w[s+8] ^ w[s+2] ^ w[s], 1); + w[s+16] = w[s]; + tmp = rot(a, 5) + f0(b, c, d) + e + w[s] + k0; + e = d; d = c; c = rot(b, 30); b = a; a = tmp; + s = (s + 1) % 16; + } + + /* t=20-39 */ + for(i = 0; i < 20; i++) { + w[s] = rot(w[s+13] ^ w[s+8] ^ w[s+2] ^ w[s], 1); + w[s+16] = w[s]; + tmp = rot(a, 5) + f1(b, c, d) + e + w[s] + k1; + e = d; d = c; c = rot(b, 30); b = a; a = tmp; + s = (s + 1) % 16; + } + + /* t=40-59 */ + for(i = 0; i < 20; i++) { + w[s] = rot(w[s+13] ^ w[s+8] ^ w[s+2] ^ w[s], 1); + w[s+16] = w[s]; + tmp = rot(a, 5) + f2(b, c, d) + e + w[s] + k2; + e = d; d = c; c = rot(b, 30); b = a; a = tmp; + s = (s + 1) % 16; + } + + /* t=60-79 */ + for(i = 0; i < 20; i++) { + w[s] = rot(w[s+13] ^ w[s+8] ^ w[s+2] ^ w[s], 1); + w[s+16] = w[s]; + tmp = rot(a, 5) + f3(b, c, d) + e + w[s] + k3; + e = d; d = c; c = rot(b, 30); b = a; a = tmp; + s = (s + 1) % 16; + } + + pms->sha1_h[0] += a, pms->sha1_h[1] += b, pms->sha1_h[2] += c, pms->sha1_h[3] += d, pms->sha1_h[4] += e; +} + +/* + * Increment sha1_size1, sha1_size2 field of sha1_state_s + */ +static INLINE void incr(sha1_state_s *pms, int v) +{ + sha1_word_t q; + + q = pms->sha1_size1 + v * BITS; + if(q < pms->sha1_size1) { + pms->sha1_size2++; + } + pms->sha1_size1 = q; +} + +/* + * Initialize sha1_state_s as FIPS specifies + */ +void sha1_init(sha1_state_s *pms) +{ + memset(pms, 0, sizeof(*pms)); + pms->sha1_h[0] = 0x67452301; /* Initialize H[0]-H[4] */ + pms->sha1_h[1] = 0xEFCDAB89; + pms->sha1_h[2] = 0x98BADCFE; + pms->sha1_h[3] = 0x10325476; + pms->sha1_h[4] = 0xC3D2E1F0; +} + +/* + * Fill block and update output when needed + */ +void sha1_update(sha1_state_s *pms, sha1_byte_t *bufp, int length) +{ + /* Is the buffer partially filled? */ + if(pms->sha1_count != 0) { + if(pms->sha1_count + length >= (signed) sizeof(pms->sha1_buf)) { /* buffer is filled enough */ + int fil = sizeof(pms->sha1_buf) - pms->sha1_count; /* length to copy */ + + memcpy(pms->sha1_buf + pms->sha1_count, bufp, fil); + sha1_update_now(pms, pms->sha1_buf); + length -= fil; + bufp += fil; + pms->sha1_count = 0; + incr(pms, fil); + } else { + memcpy(pms->sha1_buf + pms->sha1_count, bufp, length); + pms->sha1_count += length; + incr(pms, length); + return; + } + } + + /* Loop to update state */ + for(;;) { + if(length < (signed) sizeof(pms->sha1_buf)) { /* Short to fill up the buffer */ + if(length) { + memcpy(pms->sha1_buf, bufp, length); + } + pms->sha1_count = length; + incr(pms, length); + break; + } + sha1_update_now(pms, bufp); + length -= sizeof(pms->sha1_buf); + bufp += sizeof(pms->sha1_buf); + incr(pms, sizeof(pms->sha1_buf)); + } +} + +void sha1_finish(sha1_state_s *pms, sha1_byte_t output[SHA1_OUTPUT_SIZE]) +{ + int i; + sha1_byte_t buf[1]; + + /* fill a bit */ + buf[0] = 0x80; + sha1_update(pms, buf, 1); + + /* Decrement sha1_size1, sha1_size2 */ + if((pms->sha1_size1 -= BITS) == 0) { + pms->sha1_size2--; + } + + /* fill zeros */ + if(pms->sha1_count > (signed) (sizeof(pms->sha1_buf) - 2 * sizeof(sha1_word_t))) { + memset(pms->sha1_buf + pms->sha1_count, 0, sizeof(pms->sha1_buf) - pms->sha1_count); + sha1_update_now(pms, pms->sha1_buf); + pms->sha1_count = 0; + } + memset(pms->sha1_buf + pms->sha1_count, 0, + sizeof(pms->sha1_buf) - pms->sha1_count - sizeof(sha1_word_t) * 2); + + /* fill last length */ + unpackup(pms->sha1_buf + sizeof(pms->sha1_buf) - sizeof(sha1_word_t) * 2, pms->sha1_size2); + unpackup(pms->sha1_buf + sizeof(pms->sha1_buf) - sizeof(sha1_word_t) * 1, pms->sha1_size1); + + /* final update */ + sha1_update_now(pms, pms->sha1_buf); + + /* move hash value to output byte array */ + for(i = 0; i < (signed) (sizeof(pms->sha1_h)/sizeof(sha1_word_t)); i++) { + unpackup(output + i * sizeof(sha1_word_t), pms->sha1_h[i]); + } +} diff --git a/libtransmission/sha1.h b/libtransmission/sha1.h new file mode 100644 index 000000000..bae61e0d3 --- /dev/null +++ b/libtransmission/sha1.h @@ -0,0 +1,68 @@ +/* + sha1.h: Implementation of SHA-1 Secure Hash Algorithm-1 + + Based upon: NIST FIPS180-1 Secure Hash Algorithm-1 + http://www.itl.nist.gov/fipspubs/fip180-1.htm + + Non-official Japanese Translation by HIRATA Yasuyuki: + http://yasu.asuka.net/translations/SHA-1.html + + Copyright (C) 2002 vi@nwr.jp. All rights reserved. + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgement in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as beging the original software. + 3. This notice may not be removed or altered from any source distribution. + + Note: + The copyright notice above is copied from md5.h by L. Petet Deutsch + . Thank him since I'm not a good speaker of English. :) + */ +#ifndef SHA1_H +#define SHA1_H + +typedef unsigned int sha1_word_t; /* 32bits unsigned integer */ +typedef unsigned char sha1_byte_t; /* 8bits unsigned integer */ +#define BITS 8 + +/* Define the state of SHA-1 algorithm */ +typedef struct { + sha1_byte_t sha1_buf[64]; /* 512 bits */ + int sha1_count; /* How many bytes are used */ + sha1_word_t sha1_size1; /* Length counter Lower Word */ + sha1_word_t sha1_size2; /* Length counter Upper Word */ + sha1_word_t sha1_h[5]; /* Hash output */ +} sha1_state_s; +#define SHA1_OUTPUT_SIZE 20 /* in bytes */ + +/* External Functions */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* Initialize SHA-1 algorithm */ +void sha1_init(sha1_state_s *pms); + +/* Append a string to SHA-1 algorithm */ +void sha1_update(sha1_state_s *pms, sha1_byte_t *input_buffer, int length); + +/* Finish the SHA-1 algorithm and return the hash */ +void sha1_finish(sha1_state_s *pms, sha1_byte_t output[SHA1_OUTPUT_SIZE]); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/libtransmission/tracker.c b/libtransmission/tracker.c new file mode 100644 index 000000000..132e082b9 --- /dev/null +++ b/libtransmission/tracker.c @@ -0,0 +1,597 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include "transmission.h" + +struct tr_tracker_s +{ + tr_torrent_t * tor; + + char * id; + + char started; + char completed; + char stopped; + + int interval; + int seeders; + int leechers; + int hasManyPeers; + + uint64_t dateTry; + uint64_t dateOk; + +#define TC_STATUS_IDLE 1 +#define TC_STATUS_CONNECT 2 +#define TC_STATUS_RECV 4 + char status; + + int socket; + uint8_t * buf; + int size; + int pos; +}; + +static void sendQuery ( tr_tracker_t * tc ); +static void recvAnswer ( tr_tracker_t * tc ); + +tr_tracker_t * tr_trackerInit( tr_handle_t * h, tr_torrent_t * tor ) +{ + tr_tracker_t * tc; + + tc = calloc( 1, sizeof( tr_tracker_t ) ); + tc->tor = tor; + tc->id = h->id; + + tc->started = 1; + + tc->seeders = -1; + tc->leechers = -1; + + tc->status = TC_STATUS_IDLE; + tc->size = 1024; + tc->buf = malloc( tc->size ); + + return tc; +} + +static int shouldConnect( tr_tracker_t * tc ) +{ + uint64_t now = tr_date(); + + /* In any case, always wait 5 seconds between two requests */ + if( now < tc->dateTry + 5000 ) + { + return 0; + } + + /* Do we need to send an event? */ + if( tc->started || tc->completed || tc->stopped ) + { + return 1; + } + + /* Should we try and get more peers? */ + if( now > tc->dateOk + 1000 * tc->interval ) + { + return 1; + } + + /* If there is quite a lot of people on this torrent, stress + the tracker a bit until we get a decent number of peers */ + if( tc->hasManyPeers ) + { + if( tc->tor->peerCount < 5 && now > tc->dateOk + 10000 ) + { + return 1; + } + if( tc->tor->peerCount < 10 && now > tc->dateOk + 20000 ) + { + return 1; + } + if( tc->tor->peerCount < 15 && now > tc->dateOk + 30000 ) + { + return 1; + } + } + + return 0; +} + +int tr_trackerPulse( tr_tracker_t * tc ) +{ + tr_torrent_t * tor = tc->tor; + tr_info_t * inf = &tor->info; + uint64_t now = tr_date(); + + if( ( tc->status & TC_STATUS_IDLE ) && shouldConnect( tc ) ) + { + struct in_addr addr; + + if( tr_fdSocketWillCreate( tor->fdlimit, 1 ) ) + { + return 0; + } + + if( tr_netResolve( inf->trackerAddress, &addr ) ) + { + tr_fdSocketClosed( tor->fdlimit, 1 ); + return 0; + } + + tc->socket = tr_netOpen( addr, htons( inf->trackerPort ) ); + if( tc->socket < 0 ) + { + tr_fdSocketClosed( tor->fdlimit, 1 ); + return 0; + } + + tr_inf( "Tracker: connecting to %s:%d (%s)", + inf->trackerAddress, inf->trackerPort, + tc->started ? "sending 'started'" : + ( tc->completed ? "sending 'completed'" : + ( tc->stopped ? "sending 'stopped'" : + "getting peers" ) ) ); + + tc->status = TC_STATUS_CONNECT; + tc->dateTry = tr_date(); + } + + if( tc->status & TC_STATUS_CONNECT ) + { + /* We are connecting to the tracker. Try to send the query */ + sendQuery( tc ); + } + + if( tc->status & TC_STATUS_RECV ) + { + /* Try to get something */ + recvAnswer( tc ); + } + + if( tc->status > TC_STATUS_IDLE && now > tc->dateTry + 60000 ) + { + /* Give up if the request wasn't successful within 60 seconds */ + tr_inf( "Tracker: timeout reached (60 s)" ); + + tr_netClose( tc->socket ); + tr_fdSocketClosed( tor->fdlimit, 1 ); + + tc->status = TC_STATUS_IDLE; + tc->dateTry = tr_date(); + } + + return 0; +} + +void tr_trackerCompleted( tr_tracker_t * tc ) +{ + tc->started = 0; + tc->completed = 1; + tc->stopped = 0; +} + +void tr_trackerStopped( tr_tracker_t * tc ) +{ + tr_torrent_t * tor = tc->tor; + + if( tc->status > TC_STATUS_CONNECT ) + { + /* If we are already sendy a query at the moment, we need to + reconnect */ + tr_netClose( tc->socket ); + tr_fdSocketClosed( tor->fdlimit, 1 ); + tc->status = TC_STATUS_IDLE; + } + + tc->started = 0; + tc->completed = 0; + tc->stopped = 1; + + /* Even if we have connected recently, reconnect right now */ + if( tc->status & TC_STATUS_IDLE ) + { + tc->dateTry = 0; + } +} + +void tr_trackerClose( tr_tracker_t * tc ) +{ + tr_torrent_t * tor = tc->tor; + + if( tc->status > TC_STATUS_IDLE ) + { + tr_netClose( tc->socket ); + tr_fdSocketClosed( tor->fdlimit, 1 ); + } + free( tc->buf ); + free( tc ); +} + +static void sendQuery( tr_tracker_t * tc ) +{ + tr_torrent_t * tor = tc->tor; + tr_info_t * inf = &tor->info; + + char * event; + uint64_t left; + int ret; + + if( tc->started ) + event = "&event=started"; + else if( tc->completed ) + event = "&event=completed"; + else if( tc->stopped ) + event = "&event=stopped"; + else + event = ""; + + left = (uint64_t) ( tor->blockCount - tor->blockHaveCount ) * + (uint64_t) tor->blockSize; + left = MIN( left, inf->totalSize ); + + ret = snprintf( (char *) tc->buf, tc->size, + "GET %s?info_hash=%s&peer_id=%s&port=%d&uploaded=%lld&" + "downloaded=%lld&left=%lld&compact=1&numwant=50%s " + "HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", + inf->trackerAnnounce, tor->hashString, tc->id, + tor->bindPort, tor->uploaded[9], tor->downloaded[9], + left, event, inf->trackerAddress ); + + ret = tr_netSend( tc->socket, tc->buf, ret ); + if( ret & TR_NET_CLOSE ) + { + tr_inf( "Tracker: connection failed" ); + tr_netClose( tc->socket ); + tr_fdSocketClosed( tor->fdlimit, 1 ); + tc->status = TC_STATUS_IDLE; + tc->dateTry = tr_date(); + } + else if( !( ret & TR_NET_BLOCK ) ) + { + // printf( "Tracker: sent %s", tc->buf ); + tc->status = TC_STATUS_RECV; + tc->pos = 0; + } +} + +static void recvAnswer( tr_tracker_t * tc ) +{ + tr_torrent_t * tor = tc->tor; + int ret; + int i; + benc_val_t beAll; + benc_val_t * bePeers, * beFoo; + + if( tc->pos == tc->size ) + { + tc->size *= 2; + tc->buf = realloc( tc->buf, tc->size ); + } + + ret = tr_netRecv( tc->socket, &tc->buf[tc->pos], + tc->size - tc->pos ); + + if( ret & TR_NET_BLOCK ) + { + return; + } + if( !( ret & TR_NET_CLOSE ) ) + { + // printf( "got %d bytes\n", ret ); + tc->pos += ret; + return; + } + + tr_netClose( tc->socket ); + tr_fdSocketClosed( tor->fdlimit, 1 ); + // printf( "connection closed, got total %d bytes\n", tc->pos ); + + tc->status = TC_STATUS_IDLE; + tc->dateTry = tr_date(); + + if( tc->pos < 1 ) + { + /* We got nothing */ + return; + } + + /* Find the beginning of the dictionary */ + for( i = 0; i < tc->pos - 18; i++ ) + { + /* Hem */ + if( !memcmp( &tc->buf[i], "d8:interval", 11 ) || + !memcmp( &tc->buf[i], "d8:complete", 11 ) || + !memcmp( &tc->buf[i], "d14:failure reason", 18 ) ) + { + break; + } + } + + if( i >= tc->pos - 18 ) + { + tr_err( "Tracker: no dictionary in answer" ); + // printf( "%s\n", tc->buf ); + return; + } + + if( tr_bencLoad( &tc->buf[i], &beAll, NULL ) ) + { + tr_err( "Tracker: error parsing bencoded data" ); + return; + } + + // tr_bencPrint( &beAll ); + + if( ( bePeers = tr_bencDictFind( &beAll, "failure reason" ) ) ) + { + tr_err( "Tracker: %s", bePeers->val.s.s ); + tor->status |= TR_TRACKER_ERROR; + snprintf( tor->error, sizeof( tor->error ), + bePeers->val.s.s ); + goto cleanup; + } + + tor->status &= ~TR_TRACKER_ERROR; + + if( !tc->interval ) + { + /* Get the tracker interval, ignore it if it is not between + 10 sec and 5 mins */ + if( !( beFoo = tr_bencDictFind( &beAll, "interval" ) ) || + !( beFoo->type & TYPE_INT ) ) + { + tr_err( "Tracker: no 'interval' field" ); + goto cleanup; + } + + tc->interval = beFoo->val.i; + tc->interval = MIN( tc->interval, 300 ); + tc->interval = MAX( 10, tc->interval ); + + tr_inf( "Tracker: interval = %d seconds", tc->interval ); + } + + if( ( beFoo = tr_bencDictFind( &beAll, "complete" ) ) && + ( beFoo->type & TYPE_INT ) ) + { + tc->seeders = beFoo->val.i; + } + if( ( beFoo = tr_bencDictFind( &beAll, "incomplete" ) ) && + ( beFoo->type & TYPE_INT ) ) + { + tc->leechers = beFoo->val.i; + } + if( tc->seeders + tc->seeders >= 50 ) + { + tc->hasManyPeers = 1; + } + + if( !( bePeers = tr_bencDictFind( &beAll, "peers" ) ) ) + { + tr_err( "Tracker: no \"peers\" field" ); + goto cleanup; + } + + if( bePeers->type & TYPE_LIST ) + { + char * ip; + int port; + + /* Original protocol */ + tr_inf( "Tracker: got %d peers", bePeers->val.l.count ); + + for( i = 0; i < bePeers->val.l.count; i++ ) + { + beFoo = tr_bencDictFind( &bePeers->val.l.vals[i], "ip" ); + if( !beFoo ) + continue; + ip = beFoo->val.s.s; + beFoo = tr_bencDictFind( &bePeers->val.l.vals[i], "port" ); + if( !beFoo ) + continue; + port = beFoo->val.i; + + tr_peerAddOld( tor, ip, port ); + } + + if( bePeers->val.l.count >= 50 ) + { + tc->hasManyPeers = 1; + } + } + else if( bePeers->type & TYPE_STR ) + { + struct in_addr addr; + in_port_t port; + + /* "Compact" extension */ + if( bePeers->val.s.i % 6 ) + { + tr_err( "Tracker: \"peers\" of size %d", + bePeers->val.s.i ); + tr_lockUnlock( tor->lock ); + goto cleanup; + } + + tr_inf( "Tracker: got %d peers", bePeers->val.s.i / 6 ); + for( i = 0; i < bePeers->val.s.i / 6; i++ ) + { + memcpy( &addr, &bePeers->val.s.s[6*i], 4 ); + memcpy( &port, &bePeers->val.s.s[6*i+4], 2 ); + + tr_peerAddCompact( tor, addr, port, -1 ); + } + + if( bePeers->val.s.i / 6 >= 50 ) + { + tc->hasManyPeers = 1; + } + } + + /* Success */ + tc->started = 0; + tc->completed = 0; + tc->dateOk = tr_date(); + + if( tc->stopped ) + { + tor->status = TR_STATUS_STOPPED; + tc->stopped = 0; + } + +cleanup: + tr_bencFree( &beAll ); +} + +int tr_trackerScrape( tr_torrent_t * tor, int * seeders, int * leechers ) +{ + tr_info_t * inf = &tor->info; + + int s, i, ret; + uint8_t buf[1024]; + benc_val_t scrape, * val1, * val2; + struct in_addr addr; + uint64_t date; + int pos, len; + + if( !tor->scrape[0] ) + { + /* scrape not supported */ + return 1; + } + + if( tr_netResolve( inf->trackerAddress, &addr ) ) + { + return 0; + } + s = tr_netOpen( addr, htons( inf->trackerPort ) ); + if( s < 0 ) + { + return 1; + } + + len = snprintf( (char *) buf, sizeof( buf ), + "GET %s?info_hash=%s HTTP/1.1\r\n" + "Host: %s\r\n" + "Connection: close\r\n\r\n", + tor->scrape, tor->hashString, + inf->trackerAddress ); + + for( date = tr_date();; ) + { + ret = tr_netSend( s, buf, len ); + if( ret & TR_NET_CLOSE ) + { + fprintf( stderr, "Could not connect to tracker\n" ); + tr_netClose( s ); + return 1; + } + else if( ret & TR_NET_BLOCK ) + { + if( tr_date() > date + 10000 ) + { + fprintf( stderr, "Could not connect to tracker\n" ); + tr_netClose( s ); + return 1; + } + } + else + { + break; + } + tr_wait( 10 ); + } + + pos = 0; + for( date = tr_date();; ) + { + ret = tr_netRecv( s, &buf[pos], sizeof( buf ) - pos ); + if( ret & TR_NET_CLOSE ) + { + break; + } + else if( ret & TR_NET_BLOCK ) + { + if( tr_date() > date + 10000 ) + { + fprintf( stderr, "Could not read from tracker\n" ); + tr_netClose( s ); + return 1; + } + } + else + { + pos += ret; + } + tr_wait( 10 ); + } + + if( pos < 1 ) + { + fprintf( stderr, "Could not read from tracker\n" ); + tr_netClose( s ); + return 1; + } + + for( i = 0; i < ret - 8; i++ ) + { + if( !memcmp( &buf[i], "d5:files", 8 ) ) + { + break; + } + } + if( i >= ret - 8 ) + { + return 1; + } + if( tr_bencLoad( &buf[i], &scrape, NULL ) ) + { + return 1; + } + + val1 = tr_bencDictFind( &scrape, "files" ); + if( !val1 ) + { + return 1; + } + val1 = &val1->val.l.vals[1]; + if( !val1 ) + { + return 1; + } + val2 = tr_bencDictFind( val1, "complete" ); + if( !val2 ) + { + return 1; + } + *seeders = val2->val.i; + val2 = tr_bencDictFind( val1, "incomplete" ); + if( !val2 ) + { + return 1; + } + *leechers = val2->val.i; + tr_bencFree( &scrape ); + + return 0; +} diff --git a/libtransmission/tracker.h b/libtransmission/tracker.h new file mode 100644 index 000000000..2da90210d --- /dev/null +++ b/libtransmission/tracker.h @@ -0,0 +1,36 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#ifndef TR_TRACKER_H +#define TR_TRACKER_H 1 + +typedef struct tr_tracker_s tr_tracker_t; + +tr_tracker_t * tr_trackerInit ( tr_handle_t *, tr_torrent_t * ); +int tr_trackerPulse ( tr_tracker_t * ); +void tr_trackerCompleted ( tr_tracker_t * ); +void tr_trackerStopped ( tr_tracker_t * ); +void tr_trackerClose ( tr_tracker_t * ); + +int tr_trackerScrape ( tr_torrent_t *, int *, int * ); + +#endif diff --git a/libtransmission/transmission.c b/libtransmission/transmission.c new file mode 100644 index 000000000..2404d4fd7 --- /dev/null +++ b/libtransmission/transmission.c @@ -0,0 +1,541 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include "transmission.h" + +/*********************************************************************** + * Local prototypes + **********************************************************************/ +static void downloadLoop( void * ); +static float rateDownload( tr_torrent_t * ); +static float rateUpload( tr_torrent_t * ); + +/*********************************************************************** + * tr_init + *********************************************************************** + * Allocates a tr_handle_t structure and initializes a few things + **********************************************************************/ +tr_handle_t * tr_init() +{ + tr_handle_t * h; + int i, r; + + h = calloc( sizeof( tr_handle_t ), 1 ); + + /* Generate a peer id : "-TRxxyy-" + 12 random alphanumeric + characters, where xx is the major version number and yy the + minor version number (Azureus-style) */ + sprintf( h->id, "-TR%02d%02d-", VERSION_MAJOR, VERSION_MINOR ); + for( i = 8; i < 20; i++ ) + { + r = tr_rand( 36 ); + h->id[i] = ( r < 26 ) ? ( 'a' + r ) : ( '0' + r - 26 ) ; + } + + /* Don't exit when writing on a broken socket */ + signal( SIGPIPE, SIG_IGN ); + + /* Initialize rate and file descripts controls */ + h->upload = tr_uploadInit(); + h->fdlimit = tr_fdInit(); + + h->bindPort = 9090; + + return h; +} + +/*********************************************************************** + * tr_setBindPort + *********************************************************************** + * + **********************************************************************/ +void tr_setBindPort( tr_handle_t * h, int port ) +{ + /* FIXME multithread safety */ + h->bindPort = port; +} + +/*********************************************************************** + * tr_setUploadLimit + *********************************************************************** + * + **********************************************************************/ +void tr_setUploadLimit( tr_handle_t * h, int limit ) +{ + tr_uploadSetLimit( h->upload, limit ); +} + +/*********************************************************************** + * tr_torrentRates + *********************************************************************** + * + **********************************************************************/ +void tr_torrentRates( tr_handle_t * h, float * dl, float * ul ) +{ + int i; + tr_torrent_t * tor; + + *dl = 0.0; + *ul = 0.0; + + for( i = 0; i < h->torrentCount; i++ ) + { + tor = h->torrents[i]; + tr_lockLock( tor->lock ); + *dl += rateDownload( tor ); + *ul += rateUpload( tor ); + tr_lockUnlock( tor->lock ); + } +} + +/*********************************************************************** + * tr_torrentInit + *********************************************************************** + * Allocates a tr_torrent_t structure, then relies on tr_metainfoParse + * to fill it. + **********************************************************************/ +int tr_torrentInit( tr_handle_t * h, const char * path ) +{ + tr_torrent_t * tor; + tr_info_t * inf; + int i; + char * s1, * s2; + + if( h->torrentCount >= TR_MAX_TORRENT_COUNT ) + { + tr_err( "Maximum number of torrents reached" ); + return 1; + } + + tor = calloc( sizeof( tr_torrent_t ), 1 ); + inf = &tor->info; + + /* Parse torrent file */ + if( tr_metainfoParse( inf, path ) ) + { + free( tor ); + return 1; + } + + /* Make sure this torrent is not already open */ + for( i = 0; i < h->torrentCount; i++ ) + { + if( !memcmp( tor->info.hash, h->torrents[i]->info.hash, + SHA_DIGEST_LENGTH ) ) + { + tr_err( "Torrent already open" ); + free( tor ); + return 1; + } + } + + tor->status = TR_STATUS_PAUSE; + tor->id = h->id; + + /* Guess scrape URL */ + s1 = strchr( inf->trackerAnnounce, '/' ); + while( ( s2 = strchr( s1 + 1, '/' ) ) ) + { + s1 = s2; + } + s1++; + if( !strncmp( s1, "announce", 8 ) ) + { + int pre = (long) s1 - (long) inf->trackerAnnounce; + int post = strlen( inf->trackerAnnounce ) - pre - 8; + memcpy( tor->scrape, inf->trackerAnnounce, pre ); + sprintf( &tor->scrape[pre], "scrape" ); + memcpy( &tor->scrape[pre+6], &inf->trackerAnnounce[pre+8], post ); + } + + /* Escaped info hash for HTTP queries */ + for( i = 0; i < SHA_DIGEST_LENGTH; i++ ) + { + sprintf( &tor->hashString[3*i], "%%%02x", inf->hash[i] ); + } + + /* Block size: usually 16 ko, or less if we have to */ + tor->blockSize = MIN( inf->pieceSize, 1 << 14 ); + tor->blockCount = ( inf->totalSize + tor->blockSize - 1 ) / + tor->blockSize; + tor->blockHave = calloc( tor->blockCount, 1 ); + tor->bitfield = calloc( ( inf->pieceCount + 7 ) / 8, 1 ); + + tr_lockInit( &tor->lock ); + + tor->upload = h->upload; + tor->fdlimit = h->fdlimit; + + /* We have a new torrent */ + h->torrents[h->torrentCount] = tor; + (h->torrentCount)++; + + return 0; +} + +/*********************************************************************** + * tr_torrentScrape + *********************************************************************** + * Allocates a tr_torrent_t structure, then relies on tr_metainfoParse + * to fill it. + **********************************************************************/ +int tr_torrentScrape( tr_handle_t * h, int t, int * s, int * l ) +{ + return tr_trackerScrape( h->torrents[t], s, l ); +} + +void tr_torrentSetFolder( tr_handle_t * h, int t, const char * path ) +{ + tr_torrent_t * tor = h->torrents[t]; + + tor->destination = strdup( path ); +} + +char * tr_torrentGetFolder( tr_handle_t * h, int t ) +{ + tr_torrent_t * tor = h->torrents[t]; + + return tor->destination; +} + +void tr_torrentStart( tr_handle_t * h, int t ) +{ + tr_torrent_t * tor = h->torrents[t]; + uint64_t now; + int i; + + tor->status = TR_STATUS_CHECK; + tor->tracker = tr_trackerInit( h, tor ); + tor->bindPort = h->bindPort; +#ifndef BEOS_NETSERVER + /* BeOS net_server seems to be unable to set incoming connections to + non-blocking. Too bad. */ + if( !tr_fdSocketWillCreate( h->fdlimit, 0 ) ) + { + tor->bindSocket = tr_netBind( &tor->bindPort ); + } +#endif + + now = tr_date(); + for( i = 0; i < 10; i++ ) + { + tor->dates[i] = now; + } + + tor->die = 0; + tr_threadCreate( &tor->thread, downloadLoop, tor ); +} + +void tr_torrentStop( tr_handle_t * h, int t ) +{ + tr_torrent_t * tor = h->torrents[t]; + + tr_lockLock( tor->lock ); + tr_trackerStopped( tor->tracker ); + tor->status = TR_STATUS_STOPPING; + tr_lockUnlock( tor->lock ); +} + +/*********************************************************************** + * torrentReallyStop + *********************************************************************** + * Joins the download thread and frees/closes everything related to it. + **********************************************************************/ +static void torrentReallyStop( tr_handle_t * h, int t ) +{ + tr_torrent_t * tor = h->torrents[t]; + + tor->die = 1; + tr_threadJoin( tor->thread ); + tr_dbg( "Thread joined" ); + + tr_trackerClose( tor->tracker ); + + while( tor->peerCount > 0 ) + { + tr_peerRem( tor, 0 ); + } + if( tor->bindSocket > -1 ) + { + tr_netClose( tor->bindSocket ); + tr_fdSocketClosed( h->fdlimit, 0 ); + } + + memset( tor->downloaded, 0, sizeof( tor->downloaded ) ); + memset( tor->uploaded, 0, sizeof( tor->uploaded ) ); +} + +/*********************************************************************** + * tr_torrentCount + *********************************************************************** + * + **********************************************************************/ +int tr_torrentCount( tr_handle_t * h ) +{ + return h->torrentCount; +} + +int tr_torrentStat( tr_handle_t * h, tr_stat_t ** stat ) +{ + tr_stat_t * s; + tr_torrent_t * tor; + tr_info_t * inf; + int i, j, k, piece; + + if( h->torrentCount < 1 ) + { + *stat = NULL; + return 0; + } + + s = malloc( h->torrentCount * sizeof( tr_stat_t ) ); + + for( i = 0; i < h->torrentCount; i++ ) + { + tor = h->torrents[i]; + inf = &tor->info; + + tr_lockLock( tor->lock ); + + if( tor->status & TR_STATUS_STOPPED ) + { + torrentReallyStop( h, i ); + tor->status = TR_STATUS_PAUSE; + } + + memcpy( &s[i].info, &tor->info, sizeof( tr_info_t ) ); + s[i].status = tor->status; + memcpy( s[i].error, tor->error, sizeof( s[i].error ) ); + + s[i].peersTotal = 0; + s[i].peersUploading = 0; + s[i].peersDownloading = 0; + + for( j = 0; j < tor->peerCount; j++ ) + { + if( tr_peerIsConnected( tor->peers[j] ) ) + { + (s[i].peersTotal)++; + if( tr_peerIsUploading( tor->peers[j] ) ) + { + (s[i].peersUploading)++; + } + if( tr_peerIsDownloading( tor->peers[j] ) ) + { + (s[i].peersDownloading)++; + } + } + } + + s[i].progress = (float) tor->blockHaveCount / (float) tor->blockCount; + + s[i].rateDownload = rateDownload( tor ); + s[i].rateUpload = rateUpload( tor ); + + if( s[i].rateDownload < 0.1 ) + { + s[i].eta = -1; + } + else + { + s[i].eta = (float) (tor->blockCount - tor->blockHaveCount ) * + (float) tor->blockSize / s[i].rateDownload / 1024.0; + if( s[i].eta > 99 * 3600 + 59 * 60 + 59 ) + { + s[i].eta = -1; + } + } + + for( j = 0; j < 120; j++ ) + { + piece = j * inf->pieceCount / 120; + + if( tr_bitfieldHas( tor->bitfield, piece ) ) + { + s[i].pieces[j] = -1; + continue; + } + + s[i].pieces[j] = 0; + + for( k = 0; k < tor->peerCount; k++ ) + { + if( tr_peerBitfield( tor->peers[k] ) && + tr_bitfieldHas( tr_peerBitfield( tor->peers[k] ), piece ) ) + { + (s[i].pieces[j])++; + } + } + } + + s[i].downloaded = tor->downloaded[9]; + s[i].uploaded = tor->uploaded[9]; + + s[i].folder = tor->destination; + + tr_lockUnlock( tor->lock ); + } + + *stat = s; + return h->torrentCount; +} + +/*********************************************************************** + * tr_torrentClose + *********************************************************************** + * Frees memory allocated by tr_torrentInit. + **********************************************************************/ +void tr_torrentClose( tr_handle_t * h, int t ) +{ + tr_torrent_t * tor = h->torrents[t]; + tr_info_t * inf = &tor->info; + + if( tor->status & ( TR_STATUS_STOPPING | TR_STATUS_STOPPED ) ) + { + /* Join the thread first */ + tr_lockLock( tor->lock ); + torrentReallyStop( h, t ); + tr_lockUnlock( tor->lock ); + } + + h->torrentCount--; + + tr_lockClose( tor->lock ); + + if( tor->destination ) + { + free( tor->destination ); + } + free( inf->pieces ); + free( inf->files ); + free( tor->blockHave ); + free( tor->bitfield ); + free( tor ); + + memmove( &h->torrents[t], &h->torrents[t+1], + ( h->torrentCount - t ) * sizeof( void * ) ); +} + +void tr_close( tr_handle_t * h ) +{ + tr_fdClose( h->fdlimit ); + tr_uploadClose( h->upload ); + free( h ); +} + +/*********************************************************************** + * downloadLoop + **********************************************************************/ +static void downloadLoop( void * _tor ) +{ + tr_torrent_t * tor = _tor; + uint64_t date1, date2; + + tr_dbg( "Thread started" ); + +#ifdef SYS_BEOS + /* This is required because on BeOS, SIGINT is sent to each thread, + which kills them not nicely */ + signal( SIGINT, SIG_IGN ); +#endif + + tor->io = tr_ioInit( tor ); + tor->status = ( tor->blockHaveCount < tor->blockCount ) ? + TR_STATUS_DOWNLOAD : TR_STATUS_SEED; + + while( !tor->die ) + { + date1 = tr_date(); + + tr_lockLock( tor->lock ); + + /* Are we finished ? */ + if( ( tor->status & TR_STATUS_DOWNLOAD ) && + tor->blockHaveCount >= tor->blockCount ) + { + /* Done */ + tor->status = TR_STATUS_SEED; + tr_trackerCompleted( tor->tracker ); + } + + /* Receive/send messages */ + if( !( tor->status & TR_STATUS_STOPPING ) ) + { + tr_peerPulse( tor ); + } + + /* Try to get new peers or to send a message to the tracker */ + tr_trackerPulse( tor->tracker ); + + tr_lockUnlock( tor->lock ); + + if( tor->status & TR_STATUS_STOPPED ) + { + break; + } + + /* Wait up to 20 ms */ + date2 = tr_date(); + if( date2 < date1 + 20 ) + { + tr_wait( date1 + 20 - date2 ); + } + } + + tr_ioClose( tor->io ); + + tor->status = TR_STATUS_STOPPED; + + tr_dbg( "Thread exited" ); +} + +/*********************************************************************** + * rateDownload, rateUpload + **********************************************************************/ +static float rateGeneric( uint64_t * dates, uint64_t * counts ) +{ + float ret; + int i; + + ret = 0.0; + for( i = 0; i < 9; i++ ) + { + if( dates[i+1] == dates[i] ) + { + continue; + } + ret += (float) ( i + 1 ) * 1000.0 / 1024.0 * + (float) ( counts[i+1] - counts[i] ) / + (float) ( dates[i+1] - dates[i] ); + } + ret *= 1000.0 / 1024.0 / 45.0; + + return ret; +} +static float rateDownload( tr_torrent_t * tor ) +{ + return rateGeneric( tor->dates, tor->downloaded ); +} +static float rateUpload( tr_torrent_t * tor ) +{ + return rateGeneric( tor->dates, tor->uploaded ); +} diff --git a/libtransmission/transmission.h b/libtransmission/transmission.h new file mode 100644 index 000000000..cd246a7fb --- /dev/null +++ b/libtransmission/transmission.h @@ -0,0 +1,210 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#ifndef TR_TRANSMISSION_H +#define TR_TRANSMISSION_H 1 + +#include + +#define SHA_DIGEST_LENGTH 20 +#define MAX_PATH_LENGTH 1024 +#define TR_MAX_TORRENT_COUNT 50 + +/*********************************************************************** + * tr_init + *********************************************************************** + * Initializes a libtransmission instance. Returns a obscure handle to + * be passed to all functions below. + **********************************************************************/ +typedef struct tr_handle_s tr_handle_t; + +tr_handle_t * tr_init(); + +/*********************************************************************** + * tr_setBindPort + *********************************************************************** + * Sets a "start" port: everytime we start a torrent, we try to bind + * this port, then the next one and so on until we are successful. + **********************************************************************/ +void tr_setBindPort( tr_handle_t *, int ); + +/*********************************************************************** + * tr_setUploadLimit + *********************************************************************** + * Sets the total upload rate limit in KB/s + **********************************************************************/ +void tr_setUploadLimit( tr_handle_t *, int ); + +/*********************************************************************** + * tr_torrentRates + *********************************************************************** + * Gets the total download and upload rates + **********************************************************************/ +void tr_torrentRates( tr_handle_t *, float *, float * ); + +/*********************************************************************** + * tr_torrentInit + *********************************************************************** + * Opens and parses torrent file at 'path'. If the file exists and is a + * valid torrent file, returns 0 and adds it to the list of torrents + * (but doesn't start it). Returns a non-zero value otherwise. + **********************************************************************/ +int tr_torrentInit( tr_handle_t *, const char * path ); + +/*********************************************************************** + * tr_torrentScrape + *********************************************************************** + * Asks the tracker for the count of seeders and leechers. Returns 0 + * and fills 's' and 'l' if successful. Otherwise returns 1 if the + * tracker doesn't support the scrape protocol, is unreachable or + * replied with some error. tr_torrentScrape may block up to 20 seconds + * before returning. + **********************************************************************/ +int tr_torrentScrape( tr_handle_t *, int, int * s, int * l ); + +/*********************************************************************** + * tr_torrentStart + *********************************************************************** + * Starts downloading. The download is launched in a seperate thread, + * therefore tr_torrentStart returns immediately. + **********************************************************************/ +void tr_torrentSetFolder( tr_handle_t *, int, const char * ); +char * tr_torrentGetFolder( tr_handle_t *, int ); +void tr_torrentStart( tr_handle_t *, int ); + +/*********************************************************************** + * tr_torrentStop + *********************************************************************** + * Stops downloading and notices the tracker that we are leaving. The + * thread keeps running while doing so. + * The thread will eventually be joined, either: + * - by tr_torrentStat when the tracker has been successfully noticed, + * - by tr_torrentStat if the tracker could not be noticed within 60s, + * - by tr_torrentClose if you choose to remove the torrent without + * waiting any further. + **********************************************************************/ +void tr_torrentStop( tr_handle_t *, int ); + +/*********************************************************************** + * tr_torrentStat + *********************************************************************** + * Allocates an array of tr_stat_t structures, containing information + * about the current status of all open torrents (see the contents + * of tr_stat_s below). Returns the count of open torrents and sets + * (*s) to the address of the array, or NULL if no torrent is open. + * In the former case, the array belongs to the caller who is + * responsible of freeing it. + * The interface should call this function every 0.5 second or so in + * order to update itself. + **********************************************************************/ +typedef struct tr_stat_s tr_stat_t; + +int tr_torrentCount( tr_handle_t * h ); +int tr_torrentStat( tr_handle_t *, tr_stat_t ** s ); + +/*********************************************************************** + * tr_torrentClose + *********************************************************************** + * Frees memory allocated by tr_torrentInit. If the torrent was running, + * you must call tr_torrentStop() before closing it. + **********************************************************************/ +void tr_torrentClose( tr_handle_t *, int ); + +/*********************************************************************** + * tr_close + *********************************************************************** + * Frees memory allocated by tr_init. + **********************************************************************/ +void tr_close( tr_handle_t * ); + + +/*********************************************************************** + * tr_stat_s + **********************************************************************/ +typedef struct tr_file_s +{ + uint64_t length; /* Length of the file, in bytes */ + char name[MAX_PATH_LENGTH]; /* Path to the file */ +} +tr_file_t; + +typedef struct tr_info_s +{ + /* Path to torrent */ + char torrent[MAX_PATH_LENGTH]; + + /* General info */ + uint8_t hash[SHA_DIGEST_LENGTH]; + char name[MAX_PATH_LENGTH]; + + /* Tracker info */ + char trackerAddress[256]; + int trackerPort; + char trackerAnnounce[MAX_PATH_LENGTH]; + + /* Pieces info */ + int pieceSize; + int pieceCount; + uint64_t totalSize; + uint8_t * pieces; + + /* Files info */ + int fileCount; + tr_file_t * files; +} +tr_info_t; + +struct tr_stat_s +{ + tr_info_t info; + +#define TR_STATUS_CHECK 0x001 /* Checking files */ +#define TR_STATUS_DOWNLOAD 0x002 /* Downloading */ +#define TR_STATUS_SEED 0x004 /* Seeding */ +#define TR_STATUS_STOPPING 0x008 /* Sending 'stopped' to the tracker */ +#define TR_STATUS_STOPPED 0x010 /* Sent 'stopped' but thread still + running (for internal use only) */ +#define TR_STATUS_PAUSE 0x020 /* Paused */ +#define TR_TRACKER_ERROR 0x100 + int status; + char error[128]; + + float progress; + float rateDownload; + float rateUpload; + int eta; + int peersTotal; + int peersUploading; + int peersDownloading; + char pieces[120]; + + uint64_t downloaded; + uint64_t uploaded; + + char * folder; +}; + +#ifdef __TRANSMISSION__ +# include "internal.h" +#endif + +#endif diff --git a/libtransmission/upload.c b/libtransmission/upload.c new file mode 100644 index 000000000..de1b4bb79 --- /dev/null +++ b/libtransmission/upload.c @@ -0,0 +1,136 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include "transmission.h" + +#define FOO 10 + +struct tr_upload_s +{ + tr_lock_t lock; + int limit; /* Max upload rate in KB/s */ + int count; /* Number of peers currently unchoked */ + uint64_t dates[FOO]; /* The last times we uploaded something */ + int sizes[FOO]; /* How many bytes we uploaded */ +}; + +tr_upload_t * tr_uploadInit() +{ + tr_upload_t * u; + + u = calloc( sizeof( tr_upload_t ), 1 ); + tr_lockInit( &u->lock ); + + return u; +} + +void tr_uploadSetLimit( tr_upload_t * u, int limit ) +{ + tr_lockLock( u->lock ); + u->limit = limit; + tr_lockUnlock( u->lock ); +} + +int tr_uploadCanUnchoke( tr_upload_t * u ) +{ + int ret; + + tr_lockLock( u->lock ); + if( u->limit < 0 ) + { + /* Infinite number of slots */ + ret = 1; + } + else + { + /* One slot per 2 KB/s */ + ret = ( u->count < ( u->limit + 1 ) / 2 ); + } + tr_lockUnlock( u->lock ); + + return ret; +} + +void tr_uploadChoked( tr_upload_t * u ) +{ + tr_lockLock( u->lock ); + (u->count)--; + tr_lockUnlock( u->lock ); +} + +void tr_uploadUnchoked( tr_upload_t * u ) +{ + tr_lockLock( u->lock ); + (u->count)++; + tr_lockUnlock( u->lock ); +} + +int tr_uploadCanUpload( tr_upload_t * u ) +{ + int ret, i, size; + uint64_t now; + + tr_lockLock( u->lock ); + if( u->limit < 0 ) + { + /* No limit */ + ret = 1; + } + else + { + ret = 0; + size = 0; + now = tr_date(); + + /* Check the last four times we sent something and decide if + we must wait */ + for( i = 0; i < FOO; i++ ) + { + size += u->sizes[i]; + if( (uint64_t) size < 1024ULL * + u->limit * ( now - u->dates[i] ) / 1000 ) + { + ret = 1; + break; + } + } + } + tr_lockUnlock( u->lock ); + + return ret; +} + +void tr_uploadUploaded( tr_upload_t * u, int size ) +{ + tr_lockLock( u->lock ); + memmove( &u->dates[1], &u->dates[0], (FOO-1) * sizeof( uint64_t ) ); + memmove( &u->sizes[1], &u->sizes[0], (FOO-1) * sizeof( int ) ); + u->dates[0] = tr_date(); + u->sizes[0] = size; + tr_lockUnlock( u->lock ); +} + +void tr_uploadClose( tr_upload_t * u ) +{ + tr_lockClose( u->lock ); + free( u ); +} diff --git a/libtransmission/upload.h b/libtransmission/upload.h new file mode 100644 index 000000000..21c10b905 --- /dev/null +++ b/libtransmission/upload.h @@ -0,0 +1,32 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +typedef struct tr_upload_s tr_upload_t; + +tr_upload_t * tr_uploadInit(); +void tr_uploadSetLimit( tr_upload_t *, int ); +int tr_uploadCanUnchoke( tr_upload_t * ); +void tr_uploadChoked( tr_upload_t * ); +void tr_uploadUnchoked( tr_upload_t * ); +int tr_uploadCanUpload( tr_upload_t * ); +void tr_uploadUploaded( tr_upload_t *, int ); +void tr_uploadClose( tr_upload_t * ); diff --git a/libtransmission/utils.c b/libtransmission/utils.c new file mode 100644 index 000000000..35c2db108 --- /dev/null +++ b/libtransmission/utils.c @@ -0,0 +1,63 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include "transmission.h" + +void tr_msg( int level, char * msg, ... ) +{ + char string[256]; + va_list args; + static int verboseLevel = 0; + + if( !verboseLevel ) + { + char * env; + env = getenv( "TR_DEBUG" ); + verboseLevel = env ? atoi( env ) : -1; + verboseLevel = verboseLevel ? verboseLevel : -1; + } + + if( verboseLevel < 1 && level > TR_MSG_ERR ) + { + return; + } + if( verboseLevel < 2 && level > TR_MSG_INF ) + { + return; + } + + va_start( args, msg ); + vsnprintf( string, sizeof( string ), msg, args ); + va_end( args ); + fprintf( stderr, "%s\n", string ); +} + +int tr_rand( int sup ) +{ + static int init = 0; + if( !init ) + { + srand( tr_date() ); + init = 1; + } + return rand() % sup; +} diff --git a/libtransmission/utils.h b/libtransmission/utils.h new file mode 100644 index 000000000..e99aafda1 --- /dev/null +++ b/libtransmission/utils.h @@ -0,0 +1,147 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#ifndef TR_UTILS_H +#define TR_UTILS_H 1 + +#define TR_MSG_ERR 1 +#define TR_MSG_INF 2 +#define TR_MSG_DBG 4 +#define tr_err( a... ) tr_msg( TR_MSG_ERR, ## a ) +#define tr_inf( a... ) tr_msg( TR_MSG_INF, ## a ) +#define tr_dbg( a... ) tr_msg( TR_MSG_DBG, ## a ) +void tr_msg ( int level, char * msg, ... ); + +int tr_rand ( int ); + +/*********************************************************************** + * tr_date + *********************************************************************** + * Returns the current date in milliseconds + **********************************************************************/ +static inline uint64_t tr_date() +{ + struct timeval tv; + gettimeofday( &tv, NULL ); + return( (uint64_t) tv.tv_sec * 1000 + (uint64_t) tv.tv_usec / 1000 ); +} + +/*********************************************************************** + * tr_wait + *********************************************************************** + * Wait 'delay' milliseconds + **********************************************************************/ +static inline void tr_wait( uint64_t delay ) +{ +#ifdef SYS_BEOS + snooze( 1000 * delay ); +#else + usleep( 1000 * delay ); +#endif +} + +/*********************************************************************** + * tr_bitfieldHas + **********************************************************************/ +static inline int tr_bitfieldHas( uint8_t * bitfield, int piece ) +{ + return ( bitfield[ piece / 8 ] & ( 1 << ( 7 - ( piece % 8 ) ) ) ); +} + +/*********************************************************************** + * tr_bitfieldAdd + **********************************************************************/ +static inline void tr_bitfieldAdd( uint8_t * bitfield, int piece ) +{ + bitfield[ piece / 8 ] |= ( 1 << ( 7 - ( piece % 8 ) ) ); +} + +#define tr_blockPiece(a) _tr_blockPiece(tor,a) +static inline int _tr_blockPiece( tr_torrent_t * tor, int block ) +{ + tr_info_t * inf = &tor->info; + return block / ( inf->pieceSize / tor->blockSize ); +} + +#define tr_blockSize(a) _tr_blockSize(tor,a) +static inline int _tr_blockSize( tr_torrent_t * tor, int block ) +{ + tr_info_t * inf = &tor->info; + int dummy; + + if( block != tor->blockCount - 1 || + !( dummy = inf->totalSize % tor->blockSize ) ) + { + return tor->blockSize; + } + + return dummy; +} + +#define tr_blockPosInPiece(a) _tr_blockPosInPiece(tor,a) +static inline int _tr_blockPosInPiece( tr_torrent_t * tor, int block ) +{ + tr_info_t * inf = &tor->info; + return tor->blockSize * + ( block % ( inf->pieceSize / tor->blockSize ) ); +} + +#define tr_pieceCountBlocks(a) _tr_pieceCountBlocks(tor,a) +static inline int _tr_pieceCountBlocks( tr_torrent_t * tor, int piece ) +{ + tr_info_t * inf = &tor->info; + if( piece < inf->pieceCount - 1 || + !( tor->blockCount % ( inf->pieceSize / tor->blockSize ) ) ) + { + return inf->pieceSize / tor->blockSize; + } + return tor->blockCount % ( inf->pieceSize / tor->blockSize ); +} + +#define tr_pieceStartBlock(a) _tr_pieceStartBlock(tor,a) +static inline int _tr_pieceStartBlock( tr_torrent_t * tor, int piece ) +{ + tr_info_t * inf = &tor->info; + return piece * ( inf->pieceSize / tor->blockSize ); +} + +#define tr_pieceSize(a) _tr_pieceSize(tor,a) +static inline int _tr_pieceSize( tr_torrent_t * tor, int piece ) +{ + tr_info_t * inf = &tor->info; + if( piece < inf->pieceCount - 1 || + !( inf->totalSize % inf->pieceSize ) ) + { + return inf->pieceSize; + } + return inf->totalSize % inf->pieceSize; +} + +#define tr_block(a,b) _tr_block(tor,a,b) +static inline int _tr_block( tr_torrent_t * tor, int index, int begin ) +{ + tr_info_t * inf = &tor->info; + return index * ( inf->pieceSize / tor->blockSize ) + + begin / tor->blockSize; +} + +#endif diff --git a/macosx/Controller.h b/macosx/Controller.h new file mode 100644 index 000000000..8f675fbbb --- /dev/null +++ b/macosx/Controller.h @@ -0,0 +1,78 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include +#include +#include "PrefsController.h" + +@interface Controller : NSObject +{ + tr_handle_t * fHandle; + int fCount; + tr_stat_t * fStat; + int fResumeOnWake[TR_MAX_TORRENT_COUNT]; + + NSToolbar * fToolbar; + + IBOutlet PrefsController * fPrefsController; + + IBOutlet NSMenuItem * fAdvancedBarItem; + + IBOutlet NSWindow * fWindow; + IBOutlet NSTableView * fTableView; + IBOutlet NSTextField * fTotalDLField; + IBOutlet NSTextField * fTotalULField; + + IBOutlet NSPanel * fInfoPanel; + IBOutlet NSTextField * fInfoTitle; + IBOutlet NSTextField * fInfoTracker; + IBOutlet NSTextField * fInfoAnnounce; + IBOutlet NSTextField * fInfoSize; + IBOutlet NSTextField * fInfoPieces; + IBOutlet NSTextField * fInfoPieceSize; + IBOutlet NSTextField * fInfoFolder; + IBOutlet NSTextField * fInfoDownloaded; + IBOutlet NSTextField * fInfoUploaded; + + io_connect_t fRootPort; + NSArray * fFilenames; + NSTimer * fTimer; +} + +- (void) advancedChanged: (id) sender; +- (void) openShowSheet: (id) sender; +- (void) openSheetClosed: (NSOpenPanel *) s returnCode: (int) code + contextInfo: (void *) info; +- (void) stopTorrent: (id) sender; +- (void) resumeTorrent: (id) sender; +- (void) removeTorrent: (id) sender; +- (void) showInfo: (id) sender; + +- (void) updateUI: (NSTimer *) timer; +- (void) sleepCallBack: (natural_t) messageType argument: + (void *) messageArgument; + +- (void) showMainWindow: (id) sender; +- (void) linkHomepage: (id) sender; +- (void) linkForums: (id) sender; + +@end diff --git a/macosx/Controller.m b/macosx/Controller.m new file mode 100644 index 000000000..762b7510e --- /dev/null +++ b/macosx/Controller.m @@ -0,0 +1,705 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include + +#include "Controller.h" +#include "NameCell.h" +#include "ProgressCell.h" +#include "Utils.h" + +#define TOOLBAR_OPEN @"Toolbar Open" +#define TOOLBAR_RESUME @"Toolbar Resume" +#define TOOLBAR_STOP @"Toolbar Stop" +#define TOOLBAR_REMOVE @"Toolbar Remove" +#define TOOLBAR_INFO @"Toolbar Info" + +static void sleepCallBack( void * controller, io_service_t y, + natural_t messageType, void * messageArgument ) +{ + Controller * c = controller; + [c sleepCallBack: messageType argument: messageArgument]; +} + +@implementation Controller + +- (void) enableToolbarItem: (NSString *) ident flag: (BOOL) e +{ + NSArray * array = [fToolbar items]; + NSToolbarItem * item; + + if( [ident isEqualToString: TOOLBAR_OPEN] ) + { + item = [array objectAtIndex: 0]; + [item setAction: e ? @selector( openShowSheet: ) : NULL]; + } + else if( [ident isEqualToString: TOOLBAR_RESUME] ) + { + item = [array objectAtIndex: 1]; + [item setAction: e ? @selector( resumeTorrent: ) : NULL]; + } + else if( [ident isEqualToString: TOOLBAR_STOP] ) + { + item = [array objectAtIndex: 2]; + [item setAction: e ? @selector( stopTorrent: ) : NULL]; + } + else if( [ident isEqualToString: TOOLBAR_REMOVE] ) + { + item = [array objectAtIndex: 3]; + [item setAction: e ? @selector( removeTorrent: ) : NULL]; + } + else if( [ident isEqualToString: TOOLBAR_INFO] ) + { + item = [array objectAtIndex: 5]; + [item setAction: e ? @selector( showInfo: ) : NULL]; + } +} + +- (void) updateToolbar +{ + int row = [fTableView selectedRow]; + + [self enableToolbarItem: TOOLBAR_RESUME flag: NO]; + [self enableToolbarItem: TOOLBAR_STOP flag: NO]; + [self enableToolbarItem: TOOLBAR_REMOVE flag: NO]; + + if( row < 0 ) + { + return; + } + + if( fStat[row].status & + ( TR_STATUS_CHECK | TR_STATUS_DOWNLOAD | TR_STATUS_SEED ) ) + { + [self enableToolbarItem: TOOLBAR_STOP flag: YES]; + } + else + { + [self enableToolbarItem: TOOLBAR_RESUME flag: YES]; + [self enableToolbarItem: TOOLBAR_REMOVE flag: YES]; + } +} + +- (void) awakeFromNib +{ + fHandle = tr_init(); + + [fPrefsController setHandle: fHandle]; + + [fWindow setContentMinSize: NSMakeSize( 400, 120 )]; + + /* Check or uncheck menu item in respect to current preferences */ + [fAdvancedBarItem setState: [[NSUserDefaults standardUserDefaults] + boolForKey:@"UseAdvancedBar"] ? NSOnState : NSOffState]; + + fToolbar = [[NSToolbar alloc] initWithIdentifier: @"Transmission Toolbar"]; + [fToolbar setDelegate: self]; + [fToolbar setAllowsUserCustomization: YES]; + [fToolbar setAutosavesConfiguration: YES]; + [fWindow setToolbar: fToolbar]; + [fWindow setDelegate: self]; + + [self enableToolbarItem: TOOLBAR_OPEN flag: YES]; + [self enableToolbarItem: TOOLBAR_RESUME flag: NO]; + [self enableToolbarItem: TOOLBAR_STOP flag: NO]; + [self enableToolbarItem: TOOLBAR_REMOVE flag: NO]; + [self enableToolbarItem: TOOLBAR_INFO flag: YES]; + + [fTableView setDataSource: self]; + [fTableView setDelegate: self]; + + NSTableColumn * tableColumn; + NameCell * nameCell; + ProgressCell * progressCell; + + nameCell = [[NameCell alloc] init]; + progressCell = [[ProgressCell alloc] init]; + tableColumn = [fTableView tableColumnWithIdentifier: @"Name"]; + [tableColumn setDataCell: nameCell]; + [tableColumn setMinWidth: 10.0]; + [tableColumn setMaxWidth: 3000.0]; + + tableColumn = [fTableView tableColumnWithIdentifier: @"Progress"]; + [tableColumn setDataCell: progressCell]; + [tableColumn setMinWidth: 134.0]; + [tableColumn setMaxWidth: 134.0]; + + [fTableView sizeToFit]; + + [fTableView registerForDraggedTypes: [NSArray arrayWithObjects: + NSFilenamesPboardType, NULL]]; + + IONotificationPortRef notify; + io_object_t anIterator; + + /* Register for sleep notifications */ + fRootPort = IORegisterForSystemPower( self, ¬ify, sleepCallBack, + &anIterator); + if( !fRootPort ) + { + printf( "Could not IORegisterForSystemPower\n" ); + } + else + { + CFRunLoopAddSource( CFRunLoopGetCurrent(), + IONotificationPortGetRunLoopSource( notify ), + kCFRunLoopCommonModes ); + } + + NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; + + NSArray * history = [defaults arrayForKey: @"History"]; + if( history ) + { + unsigned i; + NSDictionary * dic; + NSString * torrentPath, * downloadFolder, * paused; + + for( i = 0; i < [history count]; i++ ) + { + dic = [history objectAtIndex: i]; + + torrentPath = [dic objectForKey: @"TorrentPath"]; + downloadFolder = [dic objectForKey: @"DownloadFolder"]; + paused = [dic objectForKey: @"Paused"]; + + if( !torrentPath || !downloadFolder || !paused ) + { + continue; + } + + if( tr_torrentInit( fHandle, [torrentPath UTF8String] ) ) + { + continue; + } + + tr_torrentSetFolder( fHandle, tr_torrentCount( fHandle ) - 1, + [downloadFolder UTF8String] ); + + if( [paused isEqualToString: @"NO"] ) + { + tr_torrentStart( fHandle, tr_torrentCount( fHandle ) - 1 ); + } + } + } + + /* Update the interface every 500 ms */ + fCount = 0; + fStat = NULL; + fTimer = [NSTimer scheduledTimerWithTimeInterval: 0.5 target: self + selector: @selector( updateUI: ) userInfo: NULL repeats: YES]; + [[NSRunLoop currentRunLoop] addTimer: fTimer + forMode: NSModalPanelRunLoopMode]; +} + +- (BOOL) windowShouldClose: (id) sender +{ + [fWindow orderOut: NULL]; + return NO; +} + +- (BOOL) applicationShouldHandleReopen: (NSApplication *) app + hasVisibleWindows: (BOOL) flag +{ + [self showMainWindow: NULL]; + return NO; +} + +- (NSApplicationTerminateReply) applicationShouldTerminate: + (NSApplication *) app +{ + NSMutableArray * history = [NSMutableArray + arrayWithCapacity: TR_MAX_TORRENT_COUNT]; + int i; + + /* Stop updating the interface */ + [fTimer invalidate]; + + /* Save history and stop running torrents */ + for( i = 0; i < fCount; i++ ) + { + [history addObject: [NSDictionary dictionaryWithObjectsAndKeys: + [NSString stringWithUTF8String: fStat[i].info.torrent], + @"TorrentPath", + [NSString stringWithUTF8String: tr_torrentGetFolder( fHandle, i )], + @"DownloadFolder", + ( fStat[i].status & ( TR_STATUS_CHECK | TR_STATUS_DOWNLOAD | + TR_STATUS_SEED ) ) ? @"NO" : @"YES", + @"Paused", + NULL]]; + + if( fStat[i].status & ( TR_STATUS_CHECK | + TR_STATUS_DOWNLOAD | TR_STATUS_SEED ) ) + { + tr_torrentStop( fHandle, i ); + } + } + + /* Wait for torrents to stop (5 seconds timeout) */ + NSDate * start = [NSDate date]; + while( fCount > 0 ) + { + while( [[NSDate date] timeIntervalSinceDate: start] < 5 ) + { + fCount = tr_torrentStat( fHandle, &fStat ); + if( fStat[0].status & TR_STATUS_PAUSE ) + { + break; + } + usleep( 500000 ); + } + tr_torrentClose( fHandle, 0 ); + fCount = tr_torrentStat( fHandle, &fStat ); + } + + tr_close( fHandle ); + + [[NSUserDefaults standardUserDefaults] + setObject: history forKey: @"History"]; + + return NSTerminateNow; +} + +- (void) folderChoiceClosed: (NSOpenPanel *) s returnCode: (int) code + contextInfo: (void *) info +{ + if( code != NSOKButton ) + { + tr_torrentClose( fHandle, tr_torrentCount( fHandle ) - 1 ); + [NSApp stopModal]; + return; + } + + tr_torrentSetFolder( fHandle, tr_torrentCount( fHandle ) - 1, + [[[s filenames] objectAtIndex: 0] UTF8String] ); + tr_torrentStart( fHandle, tr_torrentCount( fHandle ) - 1 ); + [NSApp stopModal]; +} + + +- (void) application: (NSApplication *) sender + openFiles: (NSArray *) filenames +{ + unsigned i; + NSUserDefaults * defaults; + NSString * downloadChoice, * downloadFolder, * torrentPath; + + defaults = [NSUserDefaults standardUserDefaults]; + downloadChoice = [defaults stringForKey: @"DownloadChoice"]; + downloadFolder = [defaults stringForKey: @"DownloadFolder"]; + + for( i = 0; i < [filenames count]; i++ ) + { + torrentPath = [filenames objectAtIndex: i]; + + if( tr_torrentInit( fHandle, [torrentPath UTF8String] ) ) + { + continue; + } + + if( [downloadChoice isEqualToString: @"Constant"] ) + { + tr_torrentSetFolder( fHandle, tr_torrentCount( fHandle ) - 1, + [downloadFolder UTF8String] ); + tr_torrentStart( fHandle, tr_torrentCount( fHandle ) - 1 ); + continue; + } + + if( [downloadChoice isEqualToString: @"Torrent"] ) + { + tr_torrentSetFolder( fHandle, tr_torrentCount( fHandle ) - 1, + [[torrentPath stringByDeletingLastPathComponent] UTF8String] ); + tr_torrentStart( fHandle, tr_torrentCount( fHandle ) - 1 ); + continue; + } + + NSOpenPanel * panel; + NSString * message; + + panel = [NSOpenPanel openPanel]; + message = [NSString stringWithFormat: + @"Select the download folder for %@", + [torrentPath lastPathComponent]]; + + [panel setPrompt: @"Select"]; + [panel setMessage: message]; + [panel setAllowsMultipleSelection: NO]; + [panel setCanChooseFiles: NO]; + [panel setCanChooseDirectories: YES]; + + [panel beginSheetForDirectory: NULL file: NULL types: NULL + modalForWindow: fWindow modalDelegate: self didEndSelector: + @selector( folderChoiceClosed:returnCode:contextInfo: ) + contextInfo: NULL]; + [NSApp runModalForWindow: panel]; + } + + [self updateUI: NULL]; +} + +- (void) advancedChanged: (id) sender +{ + NSUserDefaults * defaults = [NSUserDefaults standardUserDefaults]; + if( [fAdvancedBarItem state] == NSOnState ) + { + [fAdvancedBarItem setState: NSOffState]; + [defaults setObject:@"NO" forKey:@"UseAdvancedBar"]; + } + else + { + [fAdvancedBarItem setState: NSOnState]; + [defaults setObject:@"YES" forKey:@"UseAdvancedBar"]; + } +} + +- (void) openShowSheet: (id) sender +{ + NSOpenPanel * panel; + NSArray * fileTypes; + + panel = [NSOpenPanel openPanel]; + fileTypes = [NSArray arrayWithObject: @"torrent"]; + + [panel setAllowsMultipleSelection: YES]; + [panel setCanChooseFiles: YES]; + [panel setCanChooseDirectories: NO]; + + [panel beginSheetForDirectory: NULL file: NULL types: fileTypes + modalForWindow: fWindow modalDelegate: self didEndSelector: + @selector( openSheetClosed:returnCode:contextInfo: ) + contextInfo: NULL]; +} + +- (void) cantFindAName: (id) sender +{ + [self application: NSApp openFiles: fFilenames]; + [fFilenames release]; +} + +- (void) openSheetClosed: (NSOpenPanel *) s returnCode: (int) code + contextInfo: (void *) info +{ + if( code != NSOKButton ) + { + return; + } + + fFilenames = [[s filenames] retain]; + + [self performSelectorOnMainThread: @selector(cantFindAName:) + withObject: NULL waitUntilDone: NO]; +} + +- (void) resumeTorrent: (id) sender +{ + tr_torrentStart( fHandle, [fTableView selectedRow] ); + [self updateToolbar]; +} + +- (void) stopTorrent: (id) sender +{ + tr_torrentStop( fHandle, [fTableView selectedRow] ); + [self updateToolbar]; +} + +- (void) removeTorrent: (id) sender +{ + tr_torrentClose( fHandle, [fTableView selectedRow] ); + [self updateUI: NULL]; +} + +- (void) showInfo: (id) sender +{ + if( [fInfoPanel isVisible] ) + { + [fInfoPanel close]; + } + else + { + [fInfoPanel orderFront: sender]; + } +} + +- (void) updateUI: (NSTimer *) t +{ + float dl, ul; + int row; + + /* Update the NSTableView */ + if( fStat ) + { + free( fStat ); + } + fCount = tr_torrentStat( fHandle, &fStat ); + [fTableView reloadData]; + + /* Update the global DL/UL rates */ + tr_torrentRates( fHandle, &dl, &ul ); + [fTotalDLField setStringValue: [NSString stringWithFormat: + @"Total DL: %.2f KB/s", dl]]; + [fTotalULField setStringValue: [NSString stringWithFormat: + @"Total UL: %.2f KB/s", ul]]; + + /* Update DL/UL totals in the Info panel */ + row = [fTableView selectedRow]; + if( row > -1 ) + { + [fInfoDownloaded setStringValue: + stringForFileSize( fStat[row].downloaded )]; + [fInfoUploaded setStringValue: + stringForFileSize( fStat[row].uploaded )]; + } + + /* Must we do this? Can't remember */ + [self updateToolbar]; +} + +- (int) numberOfRowsInTableView: (NSTableView *) t +{ + return fCount; +} + +- (id) tableView: (NSTableView *) t objectValueForTableColumn: + (NSTableColumn *) tableColumn row: (int) rowIndex +{ + return NULL; +} + +- (void) tableView: (NSTableView *) t willDisplayCell: (id) cell + forTableColumn: (NSTableColumn *) tableColumn row: (int) rowIndex +{ + if( [[tableColumn identifier] isEqualToString: @"Name"] ) + { + [(NameCell *) cell setStat: &fStat[rowIndex]]; + } + else if( [[tableColumn identifier] isEqualToString: @"Progress"] ) + { + [(ProgressCell *) cell setStat: &fStat[rowIndex]]; + } +} + +- (BOOL) tableView: (NSTableView *) t acceptDrop: + (id ) info row: (int) row dropOperation: + (NSTableViewDropOperation) operation +{ + NSPasteboard * pasteboard; + + pasteboard = [info draggingPasteboard]; + if( ![[pasteboard types] containsObject: NSFilenamesPboardType] ) + { + return NO; + } + + [self application: NSApp openFiles: + [pasteboard propertyListForType: NSFilenamesPboardType]]; + + return YES; +} + +- (NSDragOperation) tableView: (NSTableView *) t validateDrop: + (id ) info proposedRow: (int) row + proposedDropOperation: (NSTableViewDropOperation) operation +{ + return NSDragOperationGeneric; +} + +- (void) tableViewSelectionDidChange: (NSNotification *) n +{ + int row = [fTableView selectedRow]; + + [self updateToolbar]; + + if( row < 0 ) + { + [fInfoTitle setStringValue: @"No torrent selected"]; + [fInfoTracker setStringValue: @""]; + [fInfoAnnounce setStringValue: @""]; + [fInfoSize setStringValue: @""]; + [fInfoPieces setStringValue: @""]; + [fInfoPieceSize setStringValue: @""]; + [fInfoFolder setStringValue: @""]; + [fInfoDownloaded setStringValue: @""]; + [fInfoUploaded setStringValue: @""]; + return; + } + + /* Update info window */ + [fInfoTitle setStringValue: [NSString stringWithCString: + fStat[row].info.name]]; + [fInfoTracker setStringValue: [NSString stringWithFormat: + @"%s:%d", fStat[row].info.trackerAddress, fStat[row].info.trackerPort]]; + [fInfoAnnounce setStringValue: [NSString stringWithCString: + fStat[row].info.trackerAnnounce]]; + [fInfoSize setStringValue: + stringForFileSize( fStat[row].info.totalSize )]; + [fInfoPieces setStringValue: [NSString stringWithFormat: @"%d", + fStat[row].info.pieceCount]]; + [fInfoPieceSize setStringValue: + stringForFileSize( fStat[row].info.pieceSize )]; + [fInfoFolder setStringValue: [[NSString stringWithUTF8String: + tr_torrentGetFolder( fHandle, row )] lastPathComponent]]; +} + +- (NSToolbarItem *) toolbar: (NSToolbar *) t itemForItemIdentifier: + (NSString *) ident willBeInsertedIntoToolbar: (BOOL) flag +{ + NSToolbarItem * item; + item = [[NSToolbarItem alloc] initWithItemIdentifier: ident]; + + [item setTarget: self]; + + if( [ident isEqualToString: TOOLBAR_OPEN] ) + { + [item setLabel: @"Open"]; + [item setToolTip: @"Open a torrent"]; + [item setImage: [NSImage imageNamed: @"Open.tiff"]]; + } + else if( [ident isEqualToString: TOOLBAR_RESUME] ) + { + [item setLabel: @"Resume"]; + [item setToolTip: @"Resume download"]; + [item setImage: [NSImage imageNamed: @"Resume.tiff"]]; + } + else if( [ident isEqualToString: TOOLBAR_STOP] ) + { + [item setLabel: @"Stop"]; + [item setToolTip: @"Stop download"]; + [item setImage: [NSImage imageNamed: @"Stop.tiff"]]; + } + else if( [ident isEqualToString: TOOLBAR_REMOVE] ) + { + [item setLabel: @"Remove"]; + [item setToolTip: @"Remove torrent from list"]; + [item setImage: [NSImage imageNamed: @"Remove.tiff"]]; + } + else if( [ident isEqualToString: TOOLBAR_INFO] ) + { + [item setLabel: @"Info"]; + [item setToolTip: @"Information"]; + [item setImage: [NSImage imageNamed: @"Info.tiff"]]; + } + else + { + [item release]; + return NULL; + } + + return item; +} + +- (NSArray *) toolbarAllowedItemIdentifiers: (NSToolbar *) t +{ + return [NSArray arrayWithObjects: + TOOLBAR_OPEN, TOOLBAR_RESUME, TOOLBAR_STOP, TOOLBAR_REMOVE, + NSToolbarFlexibleSpaceItemIdentifier, TOOLBAR_INFO, NULL]; +} + +- (NSArray *) toolbarDefaultItemIdentifiers: (NSToolbar *) t +{ + return [self toolbarAllowedItemIdentifiers: t]; +} + +- (void) sleepCallBack: (natural_t) messageType argument: + (void *) messageArgument +{ + int i; + + switch( messageType ) + { + case kIOMessageSystemWillSleep: + /* Close all connections before going to sleep and remember + we should resume when we wake up */ + for( i = 0; i < fCount; i++ ) + { + if( fStat[i].status & ( TR_STATUS_CHECK | + TR_STATUS_DOWNLOAD | TR_STATUS_SEED ) ) + { + tr_torrentStop( fHandle, i ); + fResumeOnWake[i] = 1; + } + else + { + fResumeOnWake[i] = 0; + } + } + + /* TODO: wait a few seconds to let the torrents + stop properly */ + + IOAllowPowerChange( fRootPort, (long) messageArgument ); + break; + + case kIOMessageCanSystemSleep: + /* Do not prevent idle sleep */ + /* TODO: prevent it unless there are all paused? */ + IOAllowPowerChange( fRootPort, (long) messageArgument ); + break; + + case kIOMessageSystemHasPoweredOn: + /* Resume download after we wake up */ + for( i = 0; i < fCount; i++ ) + { + if( fResumeOnWake[i] ) + { + tr_torrentStart( fHandle, i ); + } + } + [self updateToolbar]; + break; + } +} + +- (NSRect) windowWillUseStandardFrame: (NSWindow *) w + defaultFrame: (NSRect) defaultFrame +{ + NSRect rectWin, rectView; + float foo; + + rectWin = [fWindow frame]; + rectView = [[fWindow contentView] frame]; + foo = 68.0 + MAX( 1, tr_torrentCount( fHandle ) ) * 62.0 - + rectView.size.height; + + rectWin.size.height += foo; + rectWin.origin.y -= foo; + + return rectWin; +} + +- (void) showMainWindow: (id) sender +{ + [fWindow makeKeyAndOrderFront: NULL]; +} + +- (void) linkHomepage: (id) sender +{ + [[NSWorkspace sharedWorkspace] openURL: [NSURL + URLWithString:@"http://transmission.m0k.org/"]]; +} + +- (void) linkForums: (id) sender +{ + [[NSWorkspace sharedWorkspace] openURL: [NSURL + URLWithString:@"http://transmission.m0k.org/forum/"]]; +} + +@end diff --git a/macosx/English.lproj/InfoPlist.strings b/macosx/English.lproj/InfoPlist.strings new file mode 100644 index 000000000..3aa7860a1 --- /dev/null +++ b/macosx/English.lproj/InfoPlist.strings @@ -0,0 +1,4 @@ +/* Localized versions of Info.plist keys */ + +CFBundleName = "Transmission"; +NSHumanReadableCopyright = "Copyright 2005 Eric Petit"; diff --git a/macosx/English.lproj/MainMenu.nib/classes.nib b/macosx/English.lproj/MainMenu.nib/classes.nib new file mode 100644 index 000000000..481d84c75 --- /dev/null +++ b/macosx/English.lproj/MainMenu.nib/classes.nib @@ -0,0 +1,55 @@ +{ + IBClasses = ( + { + ACTIONS = { + advancedChanged = id; + linkForums = id; + linkHomepage = id; + openShowSheet = id; + removeTorrent = id; + resumeTorrent = id; + showInfo = id; + showMainWindow = id; + stopTorrent = id; + }; + CLASS = Controller; + LANGUAGE = ObjC; + OUTLETS = { + fAdvancedBarItem = NSMenuItem; + fInfoAnnounce = NSTextField; + fInfoDownloaded = NSTextField; + fInfoFolder = NSTextField; + fInfoPanel = NSPanel; + fInfoPieceSize = NSTextField; + fInfoPieces = NSTextField; + fInfoSize = NSTextField; + fInfoTitle = NSTextField; + fInfoTracker = NSTextField; + fInfoUploaded = NSTextField; + fPrefsController = PrefsController; + fTableView = NSTableView; + fTotalDLField = NSTextField; + fTotalULField = NSTextField; + fWindow = NSWindow; + }; + SUPERCLASS = NSObject; + }, + {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }, + { + ACTIONS = {cancel = id; check = id; ratio = id; save = id; show = id; }; + CLASS = PrefsController; + LANGUAGE = ObjC; + OUTLETS = { + fFolderMatrix = NSMatrix; + fFolderPopUp = NSPopUpButton; + fPortField = NSTextField; + fPrefsWindow = NSWindow; + fUploadCheck = NSButton; + fUploadField = NSTextField; + fWindow = NSWindow; + }; + SUPERCLASS = NSObject; + } + ); + IBVersion = 1; +} \ No newline at end of file diff --git a/macosx/English.lproj/MainMenu.nib/info.nib b/macosx/English.lproj/MainMenu.nib/info.nib new file mode 100644 index 000000000..141b856cb --- /dev/null +++ b/macosx/English.lproj/MainMenu.nib/info.nib @@ -0,0 +1,26 @@ + + + + + IBDocumentLocation + 204 84 361 432 0 0 1440 878 + IBEditorPositions + + 29 + 105 768 371 44 0 0 1280 832 + + IBFramework Version + 439.0 + IBOldestOS + 3 + IBOpenObjects + + 21 + 29 + 273 + 343 + + IBSystem Version + 8C46 + + diff --git a/macosx/English.lproj/MainMenu.nib/keyedobjects.nib b/macosx/English.lproj/MainMenu.nib/keyedobjects.nib new file mode 100644 index 0000000000000000000000000000000000000000..8b31fb0bc386a077cdc4d5bfe0cee29302369554 GIT binary patch literal 35882 zcmeEvcX(7q*Z9oby<4*PCc9~D`lk1?o1P#g)X+mhuOZn$B+V2;=Z;8G1W^PmAkvE< z2v}$$QpAcBu^>`KLFr9uerN98O`*J>&-Z-K@2@wo$=$niPn$F6oH=J^c34G8aaDCf z!V!cKK^#&c9;uNAIrR(4uPhi*JjzztHw1oF6_=Ogg;bYU^bIK=_N=X-x@w5QrCV-i z+ZbD6YkNaigr~lH5NVNaPWplt2n|Og z&`4B;#-P{GTWA$pi@rtQp`+*oI*EQq7tmkm61tAHSO+f;?1%kvAP&Qk*n+J%5vSnh zxE=07x%^e@Blm*kHE$7D#tZ=3?7ds;mP;~JPp5w=it}zeEcr_U5uCG6?i3H zgE!($cst&KKgYZA7x+7T03XFa;-By@_!#~b72)IfBtA<*NEFE+tw?KAvY4zO>&W|LJ=sLI zl26G_@)_Apz98R`1LS*hnEXh7BPYm7a)$gtE|JUR3b{^hlZT{^LmbCxIA_j*d zh8x9=RNt!(s(w`cq&lwpO?66jR&`Nz74F}H`?ul#163Wb<~6)C@4|cX&3J!4 zln>)$`8YnAPvM{8+w$%BE__!$kMG3~RQi)2#1G~R_(Hyzf0i%jEBG3I6hDq1&r|++ zei}cEe}$jTzsk?!7xM4$i}-i><@{QH1OEZPk^h+ggx|q`%J1g)@L%#@@dxM(Vns(P+^9=zUCFI0c5{zUz$`U~}b_1Efe)ZeL( zs()0URG(6xS6@(HLi^O$)wk9EsPC&EYOqG7;chl^mO*4(JCO~7>glM8Q zF`7h8k|tHtLeo;yUeiI7t?8-ht@=(gK=r+5pz24>FioMRRP!9%83lK0;m!ojB+XRK zG|iiug_=d0)tYsh_ciM^8#NzkKGAH~>{Mi_`AoA*vrqGd<{QnonnRkynx8Z$G$%Eu zG^aIxX#Uh()LhbB*WA$D(cI+@Y3j5ptzPS*HE4}mPpzNUUmKte(S~ZHw9(oGtwo!n zP1R;RGaBT@H(pG7!wKdu++Pm619fB9usdXBi zh>CRxUYS#%k?Ho7jlUb?=zfx1DuA{~O)a9xFNjP7~e40!jlZkBGI zZlUfS-6Gv81fRp}1Km#Dew`UUJ*fLdcTD%I?k~WAbT@Q2bq}31I^?8va&mIl^@b}y z_#8QfIfXkdajMe~)DO}R)(_F=>xbzJ^fnT$FVYX!kKlhoXY|GTXZ0gVp1uTLrTQ}c zbNWhsmA+bEqaURohzIFM=xg<(^<(s7^%L|H^^^3Hp6XxFPtknJeXgI14gj2K`sw-? z@m^Gh`ReZC z8{(VdTjJZ|0`#)DP<%&RB)%&y7MF-i#bx4hafP^2TqUj+*NE?lYsGcq`{H_WgZP2C zQQRbM7C#g}5)9eE&6)C`%B7xG3v$QSt`e-wbsC=dmqU=)HvQ5XtG5hxNx zp=cC?Vo@B5M+veU?yk7x-uWdpHgL>d-EvzO6xgb&iiZ`K6jzUFQ(jgEF1x%^aoTN4 z@~f(BRZ8pr-EzAZ7gm8IuNu%Tw_ARxO}-w~E!WK#(R@G*c284yXZcQPvFDR^h+Kta~*5&9bu*uD}dtSzq`3D2}IFRSjF zUzR`IR@o2W*+y4G!`|I;dzP11Q}Qh3CQ-7Fk}oJ3Ny%QyQA+kp8gPHoAWBLo38I`N zsU>8@`z3M+#BW83C<&w>bI$0Kc!@iNQsARhlm#0c|m!8y)YOF9pKZBs1x+3J@l$tjIt%IH1Ig;g1Vw^ zs5|O`a!^l{i}Fw}Xxtn1L48p_)E^B%1JNKf7!5%~Q9c@m3Q*w?gRxt#(u29xV@hmY z^Q%V6EGYM~@(V_SAV4Hti_3D0$J#(7$|rfn)g=w@WJ!XCD4+ENC4@J*1(h~i8GFN? z&mYa6uW!@4*j5X-`{cDR$sf*ClHHKO3^BMtpR@jNT~l3Nl|RbHy2xm7HV~<=mEJB!X?YB&}@ysXex*~dv`R+F zOY)RDN^OkT)!-_%gd5r24TjccgxD(!#Lr#|_jAzb=Mi*QaC`?GMAXXR2 zO^TJ=3@YgvqYgQ(DG!w$fHrJQf&F*#QP1+)j<(|A zBdUiOe1X-<{Nck{`|=vV!-in{^2#pd1^FeweY-MdN($jBPmu%Y`r|Y?panbyTDS5t z86e=6hZY-)aLwqF1rA0ikKs|60F?)`zy|;D%JP~rrAbh$RxsQ=47_9v%Bn!f;G%P@ zgP+$vgWJX+_z+3W4G4PNUCNYvr3|AH^&gj%mI#9~#X8|AIsnoyg}z|DzZe|^HF*&o zK|cT)GHNDB=)z{8G@eF`B-4}i=0{m?wu9bm2faDY^hRngYla7ChKJNaYSGV*HFbhw zX-}ck%g||b2AyU0890;iHFO{N5{8`hi(`cUAXBPnD$~G(FF{w(Rmo3ElG2Z%8z7gP=oY$-{y}%p zU33rK2l+ez3DseQF(#m!yD*Q{SOc8|1j?$#l%!q-+kkF@y#f<0t0q_{hl*ww!tgFG zDz;U^v)NVh(>B)6@N9NfyTW4GPr|A@9?C#=T;^28!`#xB@^!Bk*3?2h)aZUwXD7|vkMD#|OX0hQh8Fq_XX z%619JslT>LAySwWD4C@wR~3DTj$lvR3^16m7xu&- zZtI*7m&kss>FG!cRe*%!h$T1zwS))chCkp49EF2%G(d{Mu{aLL<50#JGh&Q7W@JIM zCFR4}J1J6%FsP+)DYSR)n5t@9DHw+lviYtTm4mJ%MpsrsJ?2!Lwj8J7bew@RAJ<|g zJlW7NIPHsVC57w`u+zGLg)Zz3@Xh`>`s^8{&(IRw61PH&aTaca+uB_Mcn^6n#!3lN z3@Cxs)jjZGE2FkiSG1SAB0H#c54+4&?u}*98Fx`+A;n8^jNc&!&jOG-Kz_ukIQY$B z{DR0+lpiMY#sFh*FFU|U$)W%>*a6myPU)ilIDMHzOv)XYel5X+P)p#hk&XKgBM%S7 z`FI#EkVjq-8+jmvl%TA>Q7{sEIZ0A7jD$2Pv2Tyc^5J0Ys~kdYIuJ^gfnZnXR4K~Q z?1{1m<7Jn&LiH$IyA;>r(Q<%j4=7t#R_2cZ-IM=1BsW&p6blHEmT3vrBujc? zO1%;8$1jfn$Sf<3?2|o$wm!Asd@{p7&tryPhEzmDgfAU zwg(Azla8 ze?8uSKafMR&N?FwlzK>AWDJQZ&`XK3e5}j~B~6&{lfx!syO!b2Fv5-aV>AlJ_*T44 z>IQliD0PJh)#|Fk50R?@TsN?hR}OZ{+^b85IUz0~!Q443zN+yE?2LET59#h~1P-*1 zKiSqPjP1dDSKz&PpQA7AK0r1|vaUI-(~}te%FgJwGNV0pz-6w?r8N;kh=0fBVN~0M z%RGp}t@XUMZOohO#6f)N^*tm{7ZgT*MrW2P{p09K#nW5Upi|QltrI5nA7{0&$i|uu zo$Wm?F)k^=nr5{{C#C_d4R7o{{u@5A1fO7b8v=Iw1j<%gQIcP6_fDtq>81EIK7(3< z(RNH9ZPmR%J8ad8&i#%?uQxn{&*4AtpZGk!fd9gO^p3PtS}naVZInKdwo9K& zUr66b2c@IZG3lgqS~^Q{8;aXe+=1e3iaS%>mE!Ic=TY2`;=vRTrFa;{g%lT2Jc8o$ z6#qr>MT##|e1qa!6#qlq3Y^CL8#RU*F%Ml~YzS=!kQ#`6`c_9nE0D(LK22<9(EV~p^6JT~6ZCDQ$kG2)c z_obn(lORZTg$xjq@}&Z4m^54}K0@MHOhYnBbMg#nL0ZB{VVJWaCa9t$osvw7Pf?Q5 zzfFlPztYS;>v5R0fymHG+LCspy%Gz`69zf5ut`NyA&?~_l|upcOq&t5f{~^9l_O=f zCY?yD3}5iOaltW0tK~C7RXCv22e3m5r``!Cy_K7NEySVGN5B|q0QXAdIS^)b4Gw) z9s%#=w~caRRT+(Wo(mY~1I9^!ar~1pCXu%Q;{tmZOaNpP88VQvjIObidohGf;4cOE z%K`pmfKQ(We^n#7KJSog-`o-9wdU3=SoW+bV}Rkh2_FtSvH@^xlwJf3Qy#~#SyrMC z0mH{K2UDeKfMGi0;4q1k$MrT)pdFO-k+r8U46Ev#(MkvIf}~SHO*QnA1J@qFwNIJ_ zxMn;J*O!3nYdfx&0oP21t5=VFm_+1^Qs5p$X}v4hjYg1<0OX_69DqFgX^?*g$j9uE zUj@joG01s6ZH47I{T0BcP+G4_c;5)_S%7;^dIR9jeHz^J0QWCD+<5?ZK7-po*H!}g zK-L2}`3m%_FnFruyKu7+iW`99mb4I1y!A8`cL2pbJBqgf#R8f1UXYH?87l+lFpv)U zi$osRc_jM!0`CC=?@39%Ebw8y+LV-6*>Y+JLDsdXys}h&TaT*`;OZxB16*4k!$o9k zzzwYDbgHz~!D;WdpxM>7K85n1dT@mRu1MMi;CB4{XiUP5;EHA9p9SJe81Z-kgn@ba z!=+E9odEc=#!OTI6P1*-mzn5bgXI=XTM9v2iXeAs=cPSVXk;}}CR_Dn)B+h}q|!;$UyyofIUFKUPeGa2GqcQ(J@Nv;n<{+`@b*6)-irWlh79ik!21&5 zeZ}AnlHv8Jw2dl;9?$9EfcYxGoFjb?Fu#2|%y|Iw4F;2Y8(@A1Fb^=81`U|`UR5@; zVs#)9Vur3YGfX))2ZBX_V6pTAAUO0`*V=nx8Gv8Gzyt8Z0Q?984z8+GO=*R>y1W5= z&a>c}S%hP&tVfw$X0MaAuC(RXi+&woT`&CtSbuyBtE^BjaT@{aW*O^L=_kPYvx3z_ zHu`38`sS|r)deHWa$Y&7H_#(n|JGGKO7e@#SXQLLX>!}R?c5HOApI)+CY`9Sf^oZ` z@^!qwJ=S6s1E#3M9U&dJm)p0L+oyzO?s5Ruy#~rYkkqqBWer|`G5006gWCp)l2d@- z*ZxKIIo*af+1vrSjaE9t+Vm+>W>*}Pq8z_MvC1j zHd71^AeQ0;ijyc#r8rYce`K-@yq;8PAUAacZ%{c&7vSGTHd($TPgN}}Pm!9$Cbf*U zr=|o{E-C|eg-w=KZgNWCigX#sgOmVd1NuVzRWQ<4nb9css`A7>DwA{-@~fgBC|lzU zYiw3|N#T>o^s|#`mdU)%$h^@&X7axxGmN|1gv^^DLu`&s}eV z_yMSkwNH;?m^oEFk=d?xq&;M$b%2y&$mg&W9@B-ye}%O-ccU>@im~(%_?6L`pUUqb zJL;hfHN~8aTLq2>a3`>gouY`A6eyKERTHt1q;fZLma4cuIRzO3!c;NvA%0T#mfF!) zC}`C(TCnx?Xcfi!7ie!iik6qGPr^9fj&YKL5z@aPWG6XEDKo0*)W1M@n@PDbpV}uQ zoMA^eOF<~e2*pM$D$4XPF#hu>M*Wj9zGcU_P{HUTV>C3vs3_9Ez)o3R)Pl*b6lqXj#5OsPDpK309A z+JeedTUFbTr)s-u2eYA2W80_tOtnk(xoWp+59$YLn=E-+twf=)>Qzqh^eC??W;GO+ z%VcXFSP_M#eTs^zY}IUmi#%7Y$gc#q5BIamN3-WC_E5O;rr1QW7ufj#IgO{<2dQJ# z7ZiI+=|}A8UQh57_f+4YO4$d18)znhJ9iy74mh_m8Ca}rFtM~U6dvWYvPBjRv+$6U z?#=EcOmLJJ>{STWA=P2rO7#O14%$btFU9^+48=Z@l~LI!w~M}3{bJAUW>DSGHJ00b?!M{Y6f}2oVlA@m>}GfmMqgbCQBNS;m~w!0NxXfGYGTt4U90wUr%V8wFvS zj1USuc2@sYw8Cera8p)cM5TfQZx0a?1XNGbryh2^xeDIqGTvtz82z{2Ohq1LG?NuS z82QY4Fk4z5?e;HX;jSF(HiX}I`JpWQmV?o@N)*pp$!4#i)h)hAX$9eHd)8`@49gLa zH8lK(FHssogxird9H1bTLz#wVclb)B8AQRIShK+m%~+kkq2+CUw9*o+LKoJupWL!( zqnrFhr4a4E57SkCs`M+xxzb3AdptG|uKQM0Pt_g(>ws*n}P4c!1w81 zVJnM^i_0k=Tn<+(Z<#y7W@|L3;}`Qw6i)k7Jcx1n7z%y`%OvnC9jOV52T(i^=8|$6 z#-4fN*O3$_SToD9&ee4=R%5>b70vLSaL@QT%6eyG|4rG-}uD zk#^ieexD);m^j&3L&%TGo!($f>w&_?mv5BDFuxkEKxJL$Fh%tZ`Gfo+ZU=vaV&Lab z=>)~a_O+w@j}oXpO!ZwLT~SokFnfOLbQLc^{rF$_WBjlDasD^{1b>pPzvNF-4Aaz7 zipwZ2q4;--|Dd=+f<7vjC#u#&n5ZTxVNen)ELzi`!aIOrwL!Q669Q$PI-Jc@pEPCV zVam$Gl$GM=Bt-F9`<#`BIqN$93V)Tq#$V@e@HhEe{B8ap{tkbazsKLF7#Yn zQapv?mnfb^@qCK6QoNJmJrsXS@zKX7ru<3%q#7&6OsxV=cdP-rz-Q#O>0$0sURnL9DV4Ds?bzKN*lT6%qm}7UIxF|eaRDpi zTA_;jb3?UU&DI#fDkjw-wHJn{lD~>};VgB4+6>;x$V#r7KjIB!7NYDdfG#Tn951r~u^*e! zF&0u%m@WS|EL?N2(3sqDK<<+eXE4O-=IUo0h$rbNet}|1#+;bM`b&;R)8LAgz>lk2 zsavzi!=k!9c2;-5t#B)KC;mEKpzfmXs_v%lj<=Fjb&gz`bW}?7 zOCV^L3yQ5vAZNzPXL9`#ihWr%)z#%?^1?3gVXpF$k>c{g;sSX!=BWG+C)LQ4PJ7$O?|#q9mSIk`=w#7T7*zuF047u z5#@MR-Amnjxw^NykFxHH%@Jh6*eqZWluH}lDb4$-`!84bcdUPa`m@sDN%9_Smp3G! zm5!dO$a|VBZ)*yu=)aeDp)Bv={EbKDP4SBg!_(!VJXspnyA3n$B{@D%BC5i!eIO`> zs2Q^My{yob3iFK=c>pD{f&BDInR*NlGy3`f6XZP;%!p*oW2;J@gjcc)W3mJ9D~d2? z%ZQV}_N6H6fD&0wBW=Qj-iIAN-cY{@Q$-m#E6h_om*RPfa=fixutdFp)wn@56~&O-DQgr?z-Ey$^*icC z>UY(P)k|P*xJkVfja4rLpv%=O)GM(W!NO(rD)nmgK)nV!`aSR?yVUQ4)!m@}0DZ0A zq}~i_uo)cz0+@oz%jscG+O4J(793Zx#aeKeg-gm!ut8Oj@nkpIYE1C<)#dCB3_m4( zZIhP~LKi5F8qA+;y+D$XfvzkbEss)pkUqLpn&LMo-azsD6u${Q3w`>5+`(^AyhX}z zZ5x5WEv*Ly?I!z&M5gm8@|41o1XJErdC3$kxKeL{we`T|HuZLj;o(ITFP75(lL6YP z{tWv6bN)8$ptJYCdarsXn+U%{@dDQ0Z_7SyvTOrhfQ3--$-ul3#Z^!wkYjbFKfhE1 z+Wo#}1*oMxa(?&dd zQ0tP~{4rGkeWbb6_5-Y}jR4`ER-aLyRsXI&r~X6zC#-?#X{)FLODF55+_w}jqj)XF z>nL9CYW@ro#N#Y5Bw-xNISut+FgI8KP4NmTUAb{teQk;Qiux+WD=A(@@q35VHvrL1 z^)28JQYP8$+XE6lZ} zM&pd|@bE9Aqoc1RCnsMC4i5g(*VlK3PN!>bhyBQ7P2Tbz9v(G4dh|HCYSpT`W51k}J&e3K^|-tb;tUSn!%YHsh|y}R!I{rhY+ z{bN5zjvT2QKYsk}(9qB^Pk_Y%AMKxg_SthEe)wVC#ful~9zJ|n|9bG?L7n4u`}Xa+ zJ$v@l)z;RY1HA147L$YH$0;i-Yw@~u>+1IH+gJD3Uw_qIyLPSa=FOY(>-zQU@?UoE z(xppvhYlU8TfcsNU8hc+76Sk$2N3w%Z1m{S$9L}BS$E*Tfx6?zkJnweaG~z<<;(Kx z%9SfJjf~FIr%%^?_uY4Od-v{T{2gb{+Tk#|LY6IC_7D5~=bwMBJ9+Y?-2V69f3G`t z?p)m;fBaF$t{IHqe)~<9`S;&{U$=Jc+JD%y@N(!z+=dMs?jAjQRBm_n?Af~W=g-Uh z{r&gf_16p@`;6gX_%?0YbQfU6HHNcr;ljV!=L|37iOGTS!(LafUX}l{d+aktfU_w&A%Uk6H-=Ic7VZPbTGLvncli}OV%BxL#&_Jdj^yB1EXu?$dLyCj)O0_M9pAs zIOp@vKflZPVEx0MZQS?l`rf^Ja=$SeS=*SHm^tiO2EhKq`mW88Awv!Uo`((?k0~MC zVEPGsJ!Ea!Gw^EM?@Zndu5H+`VZVZYJa`fecJGHDet1w=SokYzYk!74+xUl#mjHO# zVD>+@+u=V45AF1@88!)-@(ZdeTKDMD zg*{$YUdDR!k)K;fWvbEPSOzOD@1QRHv@Uj-# zstSNh*d>ArYpW~ZK7Jjp-G_~ouUE0_%DkR!;Qn^Fb{?)=f5WbaDc4MEohyL}Xv0Rf z`*8Uh1kB(l*Y50^P4U=GwrHr5ZS-QB5fjX<3(JSWas+wrM-^m`3d$>CR}4g!h0s=^ zk3EH)*qSt(86+mZ{lELgN-}DXbTkyMPU_=mpGFT;hlYr16N{)ex^_&HX9>OJfUr>*rEy_ll*U#4 zC&il`A=8I)Or&v#AW7pv@kjO`Nn?_OB#pNmBz^1%k~Ds5SoQpg91ZcG{x=$G>PP+? z4gEJ7`foJ!-)IO4{~{WC|Nmn&^pWf>ABliuU!@7u1g+2nX@VPN1(m!>KX6RS)e}-; zns6l*r-^c;#J0()xa~?Noh^_^Qc_|GP$hUuE>05%ek_5nUjst%r_6!vfK)y!4Z1iA zB=(et-J5EX?HdS_8mjwGqEOBl;|-enjPYkOg}dr0v@brgSXcq_$=O0pYoM?VU%zIA z;@ynG&!N)5C^U4lRkLG2%&^!3Rs#>OYC@tE`jo{5noca2&Q^G6x@x*~+ZU zD~k71{H4~hEJKsaymKDK`x-4TfO5!PO+QvX*(3k|f-Ns#Wem0<&61d`lvE*41#2)K zTVbFX42%tBNl^RZi*FcTUq80O;AtG#>NyypDb_se;NUw42Zt#>Nb#Wt4$2q@Nc7gnwK>*@dC{(%`2MOBo#8DCp52V=Ad2tea$=!C4BX9yg;c2$YnCvxX`X} zINOIU*8-~K4NG#a5*7wmu_`9)qA6t?mgI+EXKi_{W62N2KP$<>)(=~`s^uyH6gZEu zn$Kyd6u<#^PzIy;7b!;9uuxLo8Le5sHliC0jk1Ktl!e}!ca`mDcskpFhF{XW!b-Hj z&=OXp)l6ZPV9hGX%F2XN3=`rC%G=D;Gkn)=9+ zVE(FRw^D_Lg5(9c3fsSkmH(QSiuqd_SbofkwNR-1TW&V6NJ-qw`vw}Sy4N(Y2AMU4 z>gJ^;4IhDcMDqiQ(EJExUy84@n)a2)%IielOs4sTq-cJXEBIv;Uu6~iYpknE;2fSx z$WIi(i~TJ_1@OBHmgdLwVhaWu3gY|`%~>d)-;i?d!Q9a3#~IH;quyp2&gp3tLf~g z^_V-Bm)a^|bLAk|kuuWprhy%x!VReKU{t7q3Zi-%6{aRs@D3`V5Yq%79J^9eS~Y-S zQ!X2D@wc^sisH6V#1l<8>aZhjaG;7_qDMK zu%na+b}dwZ$pfVxmWpT-l@80K#F=%NvW7-!Yv014g?%{M3`*RY2n>&lpt;-;uoFky zl4&U=MoL^kO^F**Q@c6Qwt=2#M@dV$CtCG|!$hF^HP)6>D%%ikHrsXNX5Uc4KFqJK zsbuS*>$kCKy8s#8C^0cIJfB8JPD4)s8OgL0)XX8QZ8KZiDT>l(w68igv2%GIvt*3TV>|?wWR{3X|p9*_zqhRP7wj6SQ$2cbmVa z-p1b`R_y|Gg;Vo)v?I|~Qp?TJE`ud8_p~dyDE_2&4R-_8@b_T{$Q|tlEyrC!i(w7S zKO~augC!2x+O7O8?RNEcZZSWXyGk~5#oFeYAGLeA46;$XpSwlobL+YLu-520cSQTW zmgkDJu*sgkqCKk3(B#49hHOo__E+vYze4MwBIFkL4;o4exO>`jTt5z~z}gGi0qPB^ zFZhE}tpwg?j|AIlgV_>YsEZP5leApk?9fB%1WH3mG$jdajpqf~^T7a0l5SCw%+}Ds zv2H2MTkWSLRf2jHjKef}0|i)es0U7whEmcTPSt_yozPuyx=xk!EhSmzO!dOCygf{hPu@`j78(hz9|tmpufN4hX)LRzy*;~`kT1-lNWN}sa9 zgRfB1OIj~Yp(F%0aY*AO@DgTN^aM6p`kj&@N{|G5KL@h~POv!(6v6<3C0pOQ8NxG4 za-_R#B^FV!?Hy3f)WeAf;5ytX=_hTGcC!UH1a_uDVhBF;loueq_lpufXs3to$0cC~OX`wpHd2&&(8*rJl21JwoAIRlb z=nM{Wb+CUQWC*f|X&^FI0on}W6Miv>72;XPc6ptT&X=^%`BP${BvGc?DvxKK8Kw2j zWe29gswjJeqYFl(-(@FsxWF+?j?Y-MVkeGCQWvhA1fYvhR{Ozb#T2wl*|Lasu_9w6eW!`WUF)|M=XK z82CS-QV}rh;vc})B$|~y{05n-erN?!!f>h^NK}xcj|LMg151blUn^X2=%~Vc7Xrk-?f-S||kOJL%^=xf` zJ_FlDx;F6DP3fzRX!LueR=WY5$*}fJC3_98E42$i?+Z8+*{nPxV_KoKg{ZL@umL=vwmE;7JE=X)-(&co!vQvH%c47W1=qzg ze+>FP*ach$l7WTBFcR3dj;Zj1?(N6^z=6tadH{h#Pl#zCnt?cpz-^@!JOJU(udoez zGmG0Q*rV`AUc6KxcXEj2Cc|b9%MI{+3NO|~R#-ezh9t801V92f1bP}0Udh~A+rgwlq9l~@7(x>o8RF^fcu<%?nTZOeV z9icgYjf3!t;S%L8bWK9 z9svJ zfiq~AJ#2m~Xl@9Xl~~yxC^rp~9kH?8S$ZUQmLBVgp3|%Jyk4!>=(T#C-bt_53wlxS ztas5H^hUj_-c9eW_t1Olo9Rt@FTJYMAI(YMgI)VI>N)@SM4=-cYs z>D%i&K#OdBCw*sq7r5%C@2>Bm&(ZhP=j!wHz4X2Hee`|x{q+4QJ8B3Nu$&Um`{yWu zNdx%EYDyr6A4N$mC8H@BL&;c5z><%rWCA4mZB}%|?K2OPHN?xD@Ity&xG)ksZ z@**WLkC;Kp%aqKdWELf_P%@j6S1Eaok~x&TP6;@ld6d9R>J3WXq~tA1-lk*$B?~Ee zhmu8Y z1V-V9lzc?V$MV}xDA_{ER!X)}vYoQ?aX=|xvi2DzyD0gblHHW-fzbzXH*;kyk!i88 zMbzb^Emlh&)DbMQ&(T&fp98J(V3~!Sx3e~WB%x6wh}!ELS*(z*YMJY;G5HqqMXu|yfKz8t0Hq)MtrAEdyfOvw^8L7CVDvb6~hR(M27 zvo=9!O_bj@9M#*HY-K8>ABWNgGWQ%_W$s=JCq+vtNGHo8Y>F?X5rwIZuG17Yo(DG4 zB~_DWEQugb<#l}UwRas3L%*c)o8`qQB%W&Ai`` z&5tSs$M<1~y=Ic-5Ed*Z8VF~ywJX)zxT^@#H*m0@R&|-b%0=Nn$Sp_!xe+6O6W_ph zwR2Qoz#)Mw&^X6khjdaml+9n^9ug;fTfG4Sm-TQ^#8rHO5KSKVyRF*o#0Pv%Ek6~4 z3Iyj|+|y1a0T6=Yy)H+9`LtFGki|FhUmayNgG+h=5;t#P#P~E8@LlrBJ3m$fdd91@_fh=Nx#C$ zg>d*XJ6(_^T;v=HC#{y#5Rh}xv*ZzBX&|2MgkB>ma3hv4|YBEtPe;140=G*Ld@Fb@)5 zEa$Wa&R2wl&sO>H!@t;miT9;%Alb$Cil3GPpSf@r1jNIhEF%v!y;wLwQQ9tNiQpU= zNMgMsf!l5;=bFH^L&V>j9heBdvx>#u5apbe&l+);XF!kx+XKlaNK+k@euu0X>LfBRkl!155s0gfk-d+Y^8+u7HCa;iSm*a8l$HIV8zu$-d4kxfdx{a_&ll zN76sm-3 zp+*=b)C!}8F~V43oG@OPAWRe{36emC=Y`3_3&Ip(sxVEMF1#qbB+L+A7G?^wgja;w z!mGk-!W`jsVXiPwm@m8`yeYgTye%vc77FhOi-dQD#ljL{sjy5~F02q%3afPh26p)VXv@H z_(Iq(d?|b-d@Xz91y-24hn~a!@?2a2jQsjqwtgPv+#>>O!!qeF8n5(5KaoG zgww(q;jHkxa8CF`_)|D9ToC>e{uV9@mxRm072&FIO}H-H5N-;$gxkVD!X4qRa8I}| zJP;lVbs`e6NJLIliM*&5HKJD3iB6(k6hu*U7F|SxXcS#VHxYE@OG>_?=`ex&3lN`9u~7fOy%0y=n{lHVvfLCHxD!qKsgKLtdvWnToUDyDVIXIRLZ4ME}e22l*^=C zbILtKxfYabNx4>(YfZT<%C(_fTgtVgTzd-BXRagVvMJYza-AvHg>qdf*Nt-BDc6H? zITX$><8mpNN4Z{<>rJ^nl^;N#hFwOy*09Nliw#qa_(dar+i<{$iwqNuxWP5&IhPUc-Av>}|vWM!egIKQiJGhFONmh7CsirV)Q; z#BUk#Hp6QM_!Dc`Y}i-_ z#5)ZhM%>ScOO3d<0nn{6;y#9NjM&Zap#dHZG2(?r{JtU0i1!%rN+bTzkYU78M%>qk zgN)eEh|LCo4NNXDtTf~rzBXc`VWDBE5d+CyM(kt6?;7!NBi>;E7AuVyc%5NbZNy8A zc$Z@-|40G>%k9A?BGhCPODMqFaV6ODMfVUnSz5%-dV8F9ay zF;}j?ZWPOi-#OmELILrhcxVZ1k7VCmlbxk}YYqGU09X*=g@WNLi!895pbLBlO>b0) ziZm6PQSc37hc%bASSx5v+A#S3ukPAm+G1@jq$=iU*J-zCztA4k{-iyo{Y`sHdsh1g zI)i!*_qR({&EF` z(9MMJ`g&WpMz=|~55D2+Abhje3Ee5(87Grdj8hAzj!wOu3Z0&Fn&>p$X|B^Er}vz8 zIqh-U=d|DHE2nRqzH|EC>5$VAr=OjUIURR8;dB=!j|P|=`ooma0@J&0FkS1b?+@Pr zHW@5UfIykRqfB8A5ZRh0t1P zBeWAb2-(6w@K+V!nX17njRuc29{k68@D01c4}1YW;A^n|2f+3p2CM%Q*!Z(x-T#Dd z-^zh+%o+>dYxM?v1Jy3^Yw?`3*4f?J>>Tgh#<`<&C+9BC-JElsdpY-Z9^#zuT;x2$ z`B~=@=Q8IC=St^l=UV6Io!@p|>Ac$cJ?C}K>zy|`Z+1TJe8TyZ^BL#go&Rt?@BEkZ zMd#Zt$VKPk=Mv(Q;L^^egG;tcXP2%ng)XHoqg-Band$PH%j+)lT;6cm>vF{9l*`{P zHw@TdfI${vNHioHQeoV*FtjnWGqg8!Fk~CL8G0Cc87d7EK^faGPNu>lOgX2VB@ zZH8|QhYi0N&KUkQTmZ$sY`AaajZUza$=?`eOfRiKIlU!3=(_AxLo4dAfZR6VBwU6sS*TJqs zT?<@CyN-1o?>f;{a(&Zvh3iJw&8}a%e(n0L>jBq;u4i2@y54fFb5pswy0vs`?bgPv zqgz+E?ruHYa@=y=dbtgD8|OB`ZIT;xo9s5lZHe15w-s)y+}60Qb^Fk5yW7`phuu!P zopJl!?V8&|cjT^f_jC_-k9BY7-p##_`!M&(?k~H~b6@1X*nO${a`%<)tKHvs-{AhG z`%(83?ibu|dU$*IdiZ;oJ%T(!Jiv6#2pvQSnCr`oC+0)?Z>gn$3>FMKX_Du2Y?Ag_`yJwDPuIEtCV$V^Y z&wIY&xz_W2&kdd%JvVuN==rhd0naO**F0}{-tzp%^RDOpX0FX5n^~KsH_K@@u-TYq z&o`Ua?5$=?o2_Z~akCxG_L$T}|ChIi_4wFH;{=KhpryAkz?2zNx@u zGYvNtn?{;SP35NNOjV{DQ>|%?X}oEY>3P!>({$4e(=5}grq@mLO>dbNn%*@nHLWnM zHmxDAqg*ClV{t@Ac`2YaV_xAkuC-O;;~_aN^g?~&f4yeE0j_kPoRiT7&nUEbe& zpYp!!ebxKA_f7BHJ|-V;A73ATAG1%8Pl!*lPnu7rPYa*cKD~VU_zd+K=~L=6(MR%m z-sc6MsXj}6miw&qS?%+l&pMy=K6`w=^||PC*%$c|UzM-gSL++;8||CmYxPa?P4P|h z&G2pS+tD}Qx6-%Tca-mF-?6@L`!4id%mA>12cljRjJ>q-R_b1<9eDC=_ z@U8R1ew-igr|~oRMf#=qrTJy}HTN6jH^eXBufWgdH{7q-Z~A)kgUl9lqPe-bv$?Cezj>f}uz9GNnkSp5n5UUv zG|w>4G_Ns#ZT`vpi}_dcZ|1r{9LNRofto;FpgzzP=o9E47#J8H7#Wxv*fy|zV8_5t zfg=M;1Iq)S3#PT<_Y`GIc+ej2zZa9`m5z^?+o3H&bb`@rLYCj-v} zo(sGhcs)oA@(%J1@((fxwF~MHlpWMLsB2L7pq!uqL4$&x4JrvL3#tg38#F)Y&7ik~ z76vT}S{$@K=!2k7gFXxTJZMkQ`JlgoE(To=x*Bvn=w{IEU_-ELa6oV%>_H6;ZXet+ zxKnVK;BLV^f_nxR1&;`x9y~XAe(;;YZwGG={xtZr;Ln5i1n&#pAABfdQQ@P*$A-TazA5~}@Q=f{gl`Mq5xz70i}17I=feLCzYzXc_{H!m z5!#5zi0Fvei1-L=L{dabL{`L*i2R6xh@yz$5ycTD5mO^pMy!r_FJfK9`iPAYnJ@U6c^z92FfE8xTJ}xs6V4FMExCgIqGWE zt?1zBgy^j3Zqa?Ci=r!{tDFJWw2#S+$&V?BDT%3x85grE=DnD8G3#SC z#(Ws_Nz9g*?J;{}_Q!l3^KHy;F{fhA#{3a;KBg|#H#Q(PC^jTEJT@{mB{nU#S8Siy zez5~%2gMGF9Tqz#_N~|jvG2sb8@n`iMeM5B&9Mh#56Aux`%~=CvBzS6i@hCZit~>1 zjq{H)#|6iQ#%0B|jcXs59oH$YOI-K3qPS^sFUHM?n;ADdZcg0XxJ7Z><35f1EN*w) zp16H+U&ft|=i>QzO}s8%AMYG*h>wkrkGI4p#V5z7#%ILm#8<{w$B&919X~F9V!RYT zJ$`xo%J|jsYvb3&uaDmt|8@M|@t5MS#9xcQ5q~@WPJ$u9Cm|*wKEaxhl#r6pI-zGm zuY|q{{SyWzJex2sVPXPJn4B;r;njpC3Cj~!C9FwUo3Jh6n}h=ihZ2q?98EZxa5dpZ z!tI1R3HK~oisv6cyzNtWj=OD)SS zt1N3Q>n!UnA6T|nwpqTi9JBmpIb}Ix`Q56v>a2p**=n%5THURI)(~sBHOiW0?PV>p zjh|^|1A<^$+U>>)+N(i73&S=$_at z(JRp>F)}eD@tMR{iCKy561ye#NX$*NCC*EHGjT!UqQoVM%M({6zL&TzaYN$Qi9aTu zOgx?VSK`IQheal+-n;M^bK5ucXSP znxxT5`p4uTbFLhYz$kb7(lTsI?E=paJx;%A7>gv?Bse4lI zq~1?`n1<50G7nTn>9Oev>DKfP>3QjW()*HShXI@TG&f1*foP#-KIVW>2hJZ}|uK-}evpf9N0XALpOw&-YLD&+*UmFZ3_*7x~xuH~2RN zJ_w8quFx_$^pD_)M^Rux7AMux>Cj*fQ8L*e%#2=mcXy zH<$|M1xb(w#|2jeR|VGw*9A8OHwC{49tfTYo)jtzRfQTtZ6QO*6zU1}g+@Yip_R}^ zXe+!ebQgLFy@kF)Oc)`2EQ}Gx2@{3M!oP*jgi_(4a6~vJlnEzUOFvZm99&FO1GuIq(_n3k&H-YBr8%s@@m8v zc_-2%(ks$C0wQ|Eia3#2WOQU~WI|+8BtJ4WGCeXsvM91NvOKaYayar+4Vh zDOm9orVLPK zDxWIB}%DsS-GlQS8gh|lsn2_(HharXv=8pXm+$~^qpw; zXs@UnO+`u6i}L7z=)mZp==kV#^%HfhI$oWm=Bv}xnd)qHj#{YBRTrp>)urlkb(OkS zU9WCXH>q3H?dndoSlz3ZsHN&b^{{$W{Z{>6{Xspg{-pk*o>woZm(?rkAL71 zOZ{7Ys6GbeK_yTHR0B0ZZSX934m=O)gGQh+co{SWEkJ9~2DAn3!Rz2(;7#xrcpG#B zJwXoW4f=ur2muMmAPO{K0vqH41QH+*5I{j1yaxt>!QewM9E<{=fN@{~m<*0lNp z0EJ*KSO6A*C7=kb1gpVXupVp#o55DF1MC8Oz!h)@+yf85Bk)8kuT|74YgM%d+6&rC zT2rls)<(N{+ghPnx@6HJdJ3c#jP{#$O}nA}t=-oi>b3OFdM{nlWj(3`{i!ytTe_p?>J#)ydcHnYpQg{y zXX&5n1$wc*S1-}`>!tbu{g8e{|3<&3-`5}NkMzezIirG6$*63+W^^##F#ct9GTt<@ zjjo2z@EejL8s70r)!yo0 z>6T?VR?Lc92`gn`%d-Yq`PNixhBe!oV|`}Lw-#E9t)qr=JsW1Sjl79mjzVc7`~^oDt5)&KPIBGs($!raD`kZO#s7m$Td1>y$YA zodeD-=dN?ldEorxJkBkbTOqe{ZboiHSPRyH888!O!3MAqd=b6`o51F_$ z> zs)lNy+9(5MqI#$ydI7zJnxYn{6>5XpqW0(w)CqM#UC}$JJL-x2s1NFg0w{zeq#zaP z$V4`R2%!W@p{LJw#L)osKKcL+MZ?etGzyJIW6*ds3FV_{Xa<^%3Q!@MhZdqGs0gh< ztI=At9&JRM(H68F?L@_BFDgN$=pZ_Rj-qeT_vi<78vTSW#^=Wu#h1pH$5+MI#@EL; z#y7{e#&^Vb#rMQZ;-&F}@gwnL@v``d_^J4h@t@IKJV6d8@Y|$CT?@LmD|Q`=XP*Ax}Du@*XMR~dp=eB`nmx(0;M&x%-|w$Q|Mib4R!zyJOt(?j(1LJKde-7Py7(Ja?hH#4U1Hx@+8Z z?hd!aJ>Y)le(#=huePm7y8G2m&hYGh(!q99R}*qGRv zIFvY@xR|(+_$To=Sta>=vPrUCvU9Ra@~vdQBuFNcLz3f?Gn4a@MadP(&B<-ay~&bf zS@KNsQu1cE-4sg=&byfRd*0Q&>v=cxZs*<2 z`#bMp-lM!HxIC_etKh1*2Cjwc;0*j6&cY3FBitA_!Od_>{0eT1+vC@9N8A}_V;}B@ zd*U422lvB4EaC``Vt{pQVFyDTfBFT+*uxyZhX>&yco-gzN8-_VES`WT;VF1Jo{4AU zIruX?A1}g7aS>jDSK+mIJ>G~n<1Kg_-ieFx9=s2Kg-h`Pd+SXPf}ZF_yr`#nrssGu&-GFs@u-*f26`WOL%ol@k=|%;tT(}%>`nD%c(c7Z-e=x? zZ;`juTkfs$)_Uu`joxN&tGC13p9m3b$;Q{Io>&)%=z1@E$V)w}Nf z>D}?}c@Mltv>dHStI%q+Capv3(&uS?+K4u$O=xr4ingKcXb0MncBa|XN4wFU)KB}; z01Z)zDilzI+7!|_O;Suf%ISM_5FJ8?(Gm1xI);v?ljsyWoz9{Kw2;oD3+WPCL|4)^ zbRFG5H_$R@KXY&x6CX0rlT$mX&IY%yELR4xbS(v8zi(rwbO nrrV`Er{7G!mF|_!N%u*I)6xG`eJWJ=A8VEUzw+~cq}~4lS}r@P literal 0 HcmV?d00001 diff --git a/macosx/Images/Info.tiff b/macosx/Images/Info.tiff new file mode 100644 index 0000000000000000000000000000000000000000..d38cbf19a4bdae83d5b36ffa8495248dec413b0a GIT binary patch literal 19380 zcmeG^30PCd)^n4P1Pq%3f*@QFfwn?I!WuDz9sUP`Y{jK5A-RD_HnRw5)qoUmtKd?R zwhG8oUrQ}&Yq9OKhbIzPObIzG_ z&z#I;NJ;`P0s!d10XzW|SVG{9Kgh%f3d@K|nM9N!9pZqH^>{M~QT1gKEZZ9RM7bdL zdOXoWU&eWkHqg*#B4YsLfOmgc3Op0>Eam0=TSTu73#1W^)7BTn-0>g%>IX!hB4F=3u!J z`6TaKpMBwtN<@>qUk*%SB`Kq@JW2c<6_z$5Uj@2sR0RHINx^CcRTzQ^!H;iVMi7y(N#327OEsljYJ_w@va3q3aw_6w^l0= zLGuI#hX#jmd_{s#k*~kMD8x53AeiMV6bpky!oXm5PDr4)1*-27E0pxe+N)E%#?UQ_ zhT$MLN+DIK(xAmLUI05ZAT%hL9U2(M>{|!9chjdx3NdL`v_yu<)lgO*hpi{>ThY74 z)PY)pyv9Um9ONdeBp7@*aQEiEW&AVgQF(qZ7` z4K^*9h_9!9KBJ)4q+$bjv|$|7=|xJ6iTj9FCBz{4xSp^JX_Ar#A4Zvs2rk56BqhN( zr;^OZM6oJ`j0pKkfl7_xJxua8bb-erICz9;T!~z*5y*vDLNt$<7KHu+j4cch`-(Wh z!tQAyCrAiq31MiEAi$r+!TdwQm`}Ae;V4oFwJ?xtAjj^>5+dMy(-TtkU0~1Bg$Xl! z$Z;p^tt74s?uR(`_d3>3dF;WaJYyF<%$|>@h=j&*R%%sJA`=h^nV1yAjj4vOtiOJs zMMADvp^^zSJc&$@ixDvjSm;NGed%ZA(0756nr-d09r5Hcmih=%eb@1t2y&@(>q9A)27M- zoSu9guN&J0*~+BoY)pkO=uh&1`w|`~CKtlRLV4~akBszK-w+Rci^V-UIVyc>N(`!$ z!qsOqB_lF1Aqw^IWilg_N-2g$DP&5m22(Ma(dp4>N@7AI)nAi8LY)*k5-yz2t&ekwmEBS(u0AV?{i;lj7ISOsRr< zN`5JDjU|>?Z}Ce>idU#4a};pVSqfLv!k9QcY^HDCe|wDt-+1y9;)|>bm7$F}Tz`Y? zbNhA&Q8IxXb_pr<6c*-#GL~pCS%gX@DC%J?z(FV&Y-kW|03N@u9L@xaF69No3gL2C)8L{QUiG{3!WB>r z7AqCxnsS9hkHJyk!If`AkuWIJV9_w9!OXCqXZ0VGEek_u#v$t0`~LrUPd}>Reqt~l z+@LWOe|JEJ9caJ+lqM8NVT|~zIZq$8j6nx_&2Y>y#uxMdQ_RVP^PmK}&0lcF>PI!B z>kQ9V|AzU>VBkLQ=U-*c8bCdx4-JPO+!TiN=9$BfTAP#Jo%JdOFbjW8A{Q%O>&|4J zZQ|;cmPUwXYSkJ{gdOig)_m!>HfjofKV;GX6+*jEFJa~Xvd;s)cZF2w5_h6+`^xrVS52U8a zB%hInCJs;X_%6r4Ws=vYfDtJPa9su8_v^_W^;s2*<;R9!vz~2Z3vR#7hF|#-6~g=; zMq+_fjWO{(ox!qD4gA4-Pn-UCpWw!xKBlJ=YZWG98CN_8>*HxP4Y^}qEeGS$kCI_V z9S3V@s-mIezG-I9GD=?+4I09uKKQ;X&w|*y{gK$$EhJ9u#e;YpBAqAkyKZ*fBvVPL z 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 Z{{L*@Ye2Om;+qhj(4e*Co-%~t^}i9E*Yp4Y literal 0 HcmV?d00001 diff --git a/macosx/Images/Open.tiff b/macosx/Images/Open.tiff new file mode 100644 index 0000000000000000000000000000000000000000..16689c73211a870a63959aa273485b772ae4e415 GIT binary patch literal 19220 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 V%<0~f-DU#@5nKS2{Irq%D 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 literal 0 HcmV?d00001 diff --git a/macosx/Images/Resume.tiff b/macosx/Images/Resume.tiff new file mode 100644 index 0000000000000000000000000000000000000000..2c2500ede16e628429c369864e2ed24db2551f60 GIT binary patch literal 19220 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 SLcVDbsR`DM_LLzEum1&fc!RG1 literal 0 HcmV?d00001 diff --git a/macosx/Images/RevealOff.tiff b/macosx/Images/RevealOff.tiff new file mode 100644 index 0000000000000000000000000000000000000000..38caf2f92882b100c16e8eb437874033fde7ac0c GIT binary patch literal 528 zcmebEWzb?^VDxHWPzbP45s_vVa5yGlc*jVTqw(N_jTQ?S_{9a-9=mKTDLG;}LF)0M z#{xw@q1}xqHW;z+WHa-+CQO>hFe@iA#4r5Z$;U_k+t;6$?Uy{Yuts%>xT?_(k!>tC zGh7)rF1V@JwJ$p?MwRQ~2kCg#a4z;!ry~!CPiE)ZbUKAAo~e_8MaWo`^Kp`0w1dd& zyN}+M?2Xr-Fhk&wgQe*O`L7=`wV8E}32lrhjYLf6cX}TRt`FX3=BFvy@6LDV(o3eGr*Ut)cIV9I(+PI&xx@9zOY%hwH)r+Im+xkJ zFT1+*dVbQ(yW8%oXrDg)d6CiE{cYcM-it`tZF~Lv&TqvUKRfT1pk)6~KmQ!z_Z0tQ zrI6Lc@_U(w3di>S0p2X<9R*alnbzr5&tfpyu-IUyO+ghC10N#;12Y3KFd6uO1S1lg z3Cv~zivD0^hKjQSd2CQN6Ob*)0F(#nWnkc76ora|bcn&knLHWAVQfYxMkxlM2_W@M m42;rnHV2~&R4-75Q3J{RAcQzjAJ7&k`17RVbn-3pYAG~?4(C)HzPbREB{BLSe z`W0=X1#9*Rot@Q@y?MHB16w>d-?FBWZRVBlk9U|?nd1`PurkYGe& zGlAJGK+$82%usPwAdd~oW&*MW8G!OYy$lQ-jG|C+kPb1JIFl!%IE>9Gz$nE4Gy$ZZ m@f@QxoPCW^2C5e%qk&|85JDWN4`>S%a{XywU{DuefYJbHl8Au- literal 0 HcmV?d00001 diff --git a/macosx/Images/Stop.tiff b/macosx/Images/Stop.tiff new file mode 100644 index 0000000000000000000000000000000000000000..86c8f1b1d8efe9f764d3d77d5dbe2eef443c640d GIT binary patch literal 18956 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 literal 0 HcmV?d00001 diff --git a/macosx/Images/Transmission.icns b/macosx/Images/Transmission.icns new file mode 100644 index 0000000000000000000000000000000000000000..99f623e0c3353423721df41fceb99d7c1f50d094 GIT binary patch literal 58576 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 M#TpL8zwz$>1FZU2+5i9m literal 0 HcmV?d00001 diff --git a/macosx/Images/TransmissionDocument.icns b/macosx/Images/TransmissionDocument.icns new file mode 100644 index 0000000000000000000000000000000000000000..32820829724ab54a117424a9e8283587fcb44cf0 GIT binary patch literal 44289 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 q?{`FpqoC~G^Vii(O{y^yc;tLl9@_ztnKs;Xn literal 0 HcmV?d00001 diff --git a/macosx/Info.plist.in b/macosx/Info.plist.in new file mode 100644 index 000000000..07976f635 --- /dev/null +++ b/macosx/Info.plist.in @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + torrent + + CFBundleTypeIconFile + TransmissionDocument + CFBundleTypeName + BitTorrent Document + CFBundleTypeRole + Viewer + LSTypeIsPackage + + NSPersistentStoreTypeKey + XML + + + CFBundleExecutable + Transmission + CFBundleIconFile + Transmission + CFBundleIdentifier + org.m0k.transmission + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + CFBundleSignature + TR## + CFBundleVersion + %%VERSION%% + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/macosx/NameCell.h b/macosx/NameCell.h new file mode 100644 index 000000000..895f6942f --- /dev/null +++ b/macosx/NameCell.h @@ -0,0 +1,33 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include +#include + +@interface NameCell : NSCell +{ + tr_stat_t * fStat; + NSRect fRevealRect; + NSPoint fClickPoint; +} +- (void) setStat: (tr_stat_t *) stat; +@end diff --git a/macosx/NameCell.m b/macosx/NameCell.m new file mode 100644 index 000000000..c22d16d3a --- /dev/null +++ b/macosx/NameCell.m @@ -0,0 +1,167 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include "NameCell.h" +#include "Utils.h" + +@implementation NameCell + +- (void) setStat: (tr_stat_t *) stat; +{ + fStat = stat; +} + +- (void) drawWithFrame: (NSRect) cellFrame inView: (NSView *) view +{ + if( ![view lockFocusIfCanDraw] ) + { + return; + } + + NSString * nameString = NULL, * timeString = @"", * peersString = @""; + NSMutableDictionary * attributes; + attributes = [NSMutableDictionary dictionaryWithCapacity: 1]; + NSPoint pen = cellFrame.origin; + + NSString * sizeString = [NSString stringWithFormat: @" (%@)", + stringForFileSize( fStat->info.totalSize )]; + + nameString = [NSString stringWithFormat: @"%@%@", + stringFittingInWidth( fStat->info.name, cellFrame.size.width - + 10 - widthForString( sizeString, 12 ), 12 ), + sizeString]; + + if( fStat->status & TR_STATUS_PAUSE ) + { + timeString = [NSString stringWithFormat: + @"Paused (%.2f %%)", 100 * fStat->progress]; + peersString = @""; + } + else if( fStat->status & TR_STATUS_CHECK ) + { + timeString = [NSString stringWithFormat: + @"Checking existing files (%.2f %%)", 100 * fStat->progress]; + peersString = @""; + } + else if( fStat->status & TR_STATUS_DOWNLOAD ) + { + if( fStat->eta < 0 ) + { + timeString = [NSString stringWithFormat: + @"Finishing in --:--:-- (%.2f %%)", 100 * fStat->progress]; + } + else + { + timeString = [NSString stringWithFormat: + @"Finishing in %02d:%02d:%02d (%.2f %%)", + fStat->eta / 3600, ( fStat->eta / 60 ) % 60, + fStat->eta % 60, 100 * fStat->progress]; + } + peersString = [NSString stringWithFormat: + @"Downloading from %d of %d peer%s", + fStat->peersUploading, fStat->peersTotal, + ( fStat->peersTotal == 1 ) ? "" : "s"]; + } + else if( fStat->status & TR_STATUS_SEED ) + { + timeString = [NSString stringWithFormat: + @"Seeding, uploading to %d of %d peer%s", + fStat->peersDownloading, fStat->peersTotal, + ( fStat->peersTotal == 1 ) ? "" : "s"]; + peersString = @""; + } + else if( fStat->status & TR_STATUS_STOPPING ) + { + timeString = @"Stopping..."; + peersString = @""; + } + + if( ( fStat->status & ( TR_STATUS_DOWNLOAD | TR_STATUS_SEED ) ) && + ( fStat->status & TR_TRACKER_ERROR ) ) + { + peersString = [NSString stringWithFormat: @"%@%@", + @"Error: ", stringFittingInWidth( fStat->error, + cellFrame.size.width - 15 - + widthForString( @"Error: ", 10 ), 10 )]; + } + + [attributes setObject: [NSFont messageFontOfSize:12.0] + forKey: NSFontAttributeName]; + + pen.x += 5; pen.y += 5; + [nameString drawAtPoint: pen withAttributes: attributes]; + + [attributes setObject: [NSFont messageFontOfSize:10.0] + forKey: NSFontAttributeName]; + + pen.x += 5; pen.y += 20; + [timeString drawAtPoint: pen withAttributes: attributes]; + + pen.x += 0; pen.y += 15; + [peersString drawAtPoint: pen withAttributes: attributes]; + + /* "Reveal in Finder" button */ + fRevealRect = NSMakeRect( cellFrame.origin.x + cellFrame.size.width - 19, + cellFrame.origin.y + cellFrame.size.height - 19, + 14, 14 ); + NSImage * revealImage; + if( NSPointInRect( fClickPoint, fRevealRect ) ) + { + revealImage = [NSImage imageNamed: @"RevealOn.tiff"]; + } + else + { + revealImage = [NSImage imageNamed: @"RevealOff.tiff"]; + } + pen.x = fRevealRect.origin.x; + pen.y = fRevealRect.origin.y + 14; + [revealImage compositeToPoint: pen operation: NSCompositeSourceOver]; + + [view unlockFocus]; +} + +/* Track mouse as long as button is down */ +- (BOOL) startTrackingAt: (NSPoint) start inView: (NSView *) v +{ + fClickPoint = start; + return YES; +} +- (BOOL) continueTracking: (NSPoint) last at: (NSPoint) current + inView: (NSView *) v +{ + fClickPoint = current; + return YES; +} + +- (void) stopTracking: (NSPoint) last at:(NSPoint) stop + inView: (NSView *) v mouseIsUp: (BOOL) flag +{ + if( flag && NSPointInRect( stop, fRevealRect ) ) + { + /* Reveal in Finder */ + [[NSWorkspace sharedWorkspace] openFile: + [NSString stringWithUTF8String: fStat->folder]]; + } + fClickPoint = NSMakePoint(0,0); +} + +@end diff --git a/macosx/PrefsController.h b/macosx/PrefsController.h new file mode 100644 index 000000000..fbbc7427c --- /dev/null +++ b/macosx/PrefsController.h @@ -0,0 +1,49 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include +#include + +@interface PrefsController : NSObject + +{ + tr_handle_t * fHandle; + + IBOutlet NSWindow * fWindow; + IBOutlet NSWindow * fPrefsWindow; + IBOutlet NSMatrix * fFolderMatrix; + IBOutlet NSPopUpButton * fFolderPopUp; + IBOutlet NSTextField * fPortField; + IBOutlet NSButton * fUploadCheck; + IBOutlet NSTextField * fUploadField; + + NSString * fDownloadFolder; +} + +- (void) setHandle: (tr_handle_t *) handle; +- (void) show: (id) sender; +- (void) ratio: (id) sender; +- (void) check: (id) sender; +- (void) cancel: (id) sender; +- (void) save: (id) sender; + +@end diff --git a/macosx/PrefsController.m b/macosx/PrefsController.m new file mode 100644 index 000000000..d451f132a --- /dev/null +++ b/macosx/PrefsController.m @@ -0,0 +1,374 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include "PrefsController.h" + +@interface PrefsController (Private) + +- (void) folderSheetShow: (id) sender; +- (void) folderSheetClosed: (NSOpenPanel *) s returnCode: (int) code + contextInfo: (void *) info; +- (void) loadSettings; +- (void) saveSettings; +- (void) updatePopUp; + +@end + +@implementation PrefsController + +/*********************************************************************** + * setHandle + *********************************************************************** + * + **********************************************************************/ +- (void) setHandle: (tr_handle_t *) handle +{ + NSUserDefaults * defaults; + NSDictionary * appDefaults; + + fHandle = handle; + + /* Register defaults settings: + - Simple bar + - Always download to Desktop + - Port 9090 + - 20 KB/s upload limit */ + NSString * desktopPath + = [NSString stringWithFormat: @"%@/Desktop", + NSHomeDirectory()]; + + defaults = [NSUserDefaults standardUserDefaults]; + appDefaults = [NSDictionary dictionaryWithObjectsAndKeys: + @"NO", @"UseAdvancedBar", + @"Constant", @"DownloadChoice", + desktopPath, @"DownloadFolder", + @"9090", @"BindPort", + @"20", @"UploadLimit", + NULL]; + [defaults registerDefaults: appDefaults]; + + /* Apply settings */ + tr_setBindPort( fHandle, [defaults integerForKey: @"BindPort"] ); + tr_setUploadLimit( fHandle, [defaults integerForKey: @"UploadLimit"] ); +} + +/*********************************************************************** + * show + *********************************************************************** + * + **********************************************************************/ +- (void) show: (id) sender +{ + NSRect mainFrame; + NSRect prefsFrame; + NSRect screenRect; + NSPoint point; + + [self loadSettings]; + + /* Place the window */ + mainFrame = [fWindow frame]; + prefsFrame = [fPrefsWindow frame]; + screenRect = [[NSScreen mainScreen] visibleFrame]; + point.x = mainFrame.origin.x + mainFrame.size.width / 2 - + prefsFrame.size.width / 2; + point.y = mainFrame.origin.y + mainFrame.size.height - 30; + + /* Make sure it is in the screen */ + if( point.x < screenRect.origin.x ) + { + point.x = screenRect.origin.x; + } + if( point.x + prefsFrame.size.width > + screenRect.origin.x + screenRect.size.width ) + { + point.x = screenRect.origin.x + + screenRect.size.width - prefsFrame.size.width; + } + if( point.y - prefsFrame.size.height < screenRect.origin.y ) + { + point.y = screenRect.origin.y + prefsFrame.size.height; + } + + [fPrefsWindow setFrameTopLeftPoint: point]; + [fPrefsWindow makeKeyAndOrderFront: NULL]; +} + +/*********************************************************************** + * ratio + *********************************************************************** + * + **********************************************************************/ +- (void) ratio: (id) sender +{ + [fFolderPopUp setEnabled: ![fFolderMatrix selectedRow]]; +} + +/*********************************************************************** + * check + *********************************************************************** + * + **********************************************************************/ +- (void) check: (id) sender +{ + if( [fUploadCheck state] == NSOnState ) + { + [fUploadField setEnabled: YES]; + } + else + { + [fUploadField setEnabled: NO]; + [fUploadField setStringValue: @""]; + } +} + +/*********************************************************************** + * cancel + *********************************************************************** + * Discards changes and closes the Preferences window + **********************************************************************/ +- (void) cancel: (id) sender +{ + [fDownloadFolder release]; + [fPrefsWindow close]; +} + +/*********************************************************************** + * save + *********************************************************************** + * Checks the user-defined options. If they are correct, saves settings + * and closes the Preferences window. Otherwise corrects them and leaves + * the window open + **********************************************************************/ +- (void) save: (id) sender +{ + int bindPort; + int uploadLimit; + + /* Bind port */ + bindPort = [fPortField intValue]; + bindPort = MAX( 1, bindPort ); + bindPort = MIN( bindPort, 65535 ); + + if( ![[fPortField stringValue] isEqualToString: + [NSString stringWithFormat: @"%d", bindPort]] ) + { + [fPortField setIntValue: bindPort]; + return; + } + + /* Upload limit */ + if( [fUploadCheck state] == NSOnState ) + { + uploadLimit = [fUploadField intValue]; + uploadLimit = MAX( 0, uploadLimit ); + + if( ![[fUploadField stringValue] isEqualToString: + [NSString stringWithFormat: @"%d", uploadLimit]] ) + { + [fUploadField setIntValue: uploadLimit]; + return; + } + } + + [self saveSettings]; + [self cancel: NULL]; +} + +@end /* @implementation PrefsController */ + +@implementation PrefsController (Private) + +- (void) folderSheetShow: (id) sender +{ + NSOpenPanel * panel; + + panel = [NSOpenPanel openPanel]; + + [panel setPrompt: @"Select"]; + [panel setAllowsMultipleSelection: NO]; + [panel setCanChooseFiles: NO]; + [panel setCanChooseDirectories: YES]; + + [panel beginSheetForDirectory: NULL file: NULL types: NULL + modalForWindow: fPrefsWindow modalDelegate: self didEndSelector: + @selector( folderSheetClosed:returnCode:contextInfo: ) + contextInfo: NULL]; +} + +- (void) folderSheetClosed: (NSOpenPanel *) s returnCode: (int) code + contextInfo: (void *) info +{ + [fFolderPopUp selectItemAtIndex: 0]; + + if( code != NSOKButton ) + { + return; + } + + [fDownloadFolder release]; + fDownloadFolder = [[s filenames] objectAtIndex: 0]; + [fDownloadFolder retain]; + + [self updatePopUp]; +} + +/*********************************************************************** + * loadSettings + *********************************************************************** + * Update the interface with the current settings + **********************************************************************/ +- (void) loadSettings +{ + NSUserDefaults * defaults; + NSString * downloadChoice; + int uploadLimit; + + /* Fill with current settings */ + defaults = [NSUserDefaults standardUserDefaults]; + + /* Download folder selection */ + downloadChoice = [defaults stringForKey: @"DownloadChoice"]; + fDownloadFolder = [defaults stringForKey: @"DownloadFolder"]; + [fDownloadFolder retain]; + + if( [downloadChoice isEqualToString: @"Constant"] ) + { + [fFolderMatrix selectCellAtRow: 0 column: 0]; + } + else if( [downloadChoice isEqualToString: @"Torrent"] ) + { + [fFolderMatrix selectCellAtRow: 1 column: 0]; + } + else + { + [fFolderMatrix selectCellAtRow: 2 column: 0]; + } + [self ratio: NULL]; + [self updatePopUp]; + + [fPortField setIntValue: [defaults integerForKey: @"BindPort"]]; + + uploadLimit = [defaults integerForKey: @"UploadLimit"]; + if( uploadLimit < 0 ) + { + [fUploadCheck setState: NSOffState]; + } + else + { + [fUploadCheck setState: NSOnState]; + [fUploadField setIntValue: uploadLimit]; + } + [self check: NULL]; +} + +/*********************************************************************** + * saveSettings + *********************************************************************** + * + **********************************************************************/ +- (void) saveSettings +{ + NSUserDefaults * defaults; + int bindPort; + int uploadLimit; + + defaults = [NSUserDefaults standardUserDefaults]; + + /* Download folder */ + switch( [fFolderMatrix selectedRow] ) + { + case 0: + [defaults setObject: @"Constant" forKey: @"DownloadChoice"]; + break; + case 1: + [defaults setObject: @"Torrent" forKey: @"DownloadChoice"]; + break; + case 2: + [defaults setObject: @"Ask" forKey: @"DownloadChoice"]; + break; + } + [defaults setObject: fDownloadFolder forKey: @"DownloadFolder"]; + + /* Bind port */ + bindPort = [fPortField intValue]; + tr_setBindPort( fHandle, bindPort ); + [defaults setObject: [NSString stringWithFormat: @"%d", bindPort] + forKey: @"BindPort"]; + + /* Upload limit */ + if( [fUploadCheck state] == NSOnState ) + { + uploadLimit = [fUploadField intValue]; + } + else + { + uploadLimit = -1; + } + tr_setUploadLimit( fHandle, uploadLimit ); + [defaults setObject: [NSString stringWithFormat: @"%d", uploadLimit] + forKey: @"UploadLimit"]; +} + +/*********************************************************************** + * updatePopUp + *********************************************************************** + * Uses fDownloadFolder to update the displayed folder name and icon + **********************************************************************/ +- (void) updatePopUp +{ + NSMenuItem * menuItem; + NSImage * image32, * image16; + + /* Set up the pop up */ + [fFolderPopUp removeAllItems]; + [fFolderPopUp addItemWithTitle: @""]; + [[fFolderPopUp menu] addItem: [NSMenuItem separatorItem]]; + [fFolderPopUp addItemWithTitle: @"Other..."]; + + menuItem = (NSMenuItem *) [fFolderPopUp lastItem]; + [menuItem setTarget: self]; + [menuItem setAction: @selector( folderSheetShow: )]; + + /* Get the icon for the folder */ + image32 = [[NSWorkspace sharedWorkspace] iconForFile: + fDownloadFolder]; + image16 = [[NSImage alloc] initWithSize: NSMakeSize(16,16)]; + + /* 32x32 -> 16x16 scaling */ + [image16 lockFocus]; + [[NSGraphicsContext currentContext] + setImageInterpolation: NSImageInterpolationHigh]; + [image32 drawInRect: NSMakeRect(0,0,16,16) + fromRect: NSMakeRect(0,0,32,32) operation: NSCompositeCopy + fraction: 1.0]; + [image16 unlockFocus]; + + /* Update the menu item */ + menuItem = (NSMenuItem *) [fFolderPopUp itemAtIndex: 0]; + [menuItem setTitle: [fDownloadFolder lastPathComponent]]; + [menuItem setImage: image16]; + + [image16 release]; +} + +@end /* @implementation PrefsController (Private) */ diff --git a/macosx/ProgressCell.h b/macosx/ProgressCell.h new file mode 100644 index 000000000..bd25b19a9 --- /dev/null +++ b/macosx/ProgressCell.h @@ -0,0 +1,43 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include +#include + +@interface ProgressCell : NSCell +{ + tr_stat_t * fStat; + + NSString * fProgressString; + NSString * fDlString; + NSString * fUlString; + + NSBitmapImageRep * fBgBmp; + NSImage * fImg; + NSBitmapImageRep * fBmp; +} +- (id) init; +- (void) setStat: (tr_stat_t *) stat; +- (void) buildSimpleBar; +- (void) buildAdvancedBar; +- (void) drawWithFrame: (NSRect) cellFrame inView: (NSView *) view; +@end diff --git a/macosx/ProgressCell.m b/macosx/ProgressCell.m new file mode 100644 index 000000000..182ff43c8 --- /dev/null +++ b/macosx/ProgressCell.m @@ -0,0 +1,257 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include "ProgressCell.h" + +#if 0 +/* Coefficients for the "3D effect" */ +static float kBarCoeffs[] = + { 0.49, 0.73, 0.84, 0.85, 0.84, 0.79, 0.78, + 0.82, 0.86, 0.91, 0.93, 0.95, 0.96, 0.71 }; +#endif + +/* 255, 100, 80 */ +static uint32_t kRed[] = + { 0x7C3127FF, 0xBA493AFF, 0xD65443FF, 0xD85544FF, 0xD65443FF, + 0xC94F3FFF, 0xC64E3EFF, 0xD15241FF, 0xDB5644FF, 0xE85B48FF, + 0xED5D4AFF, 0xF25F4CFF, 0xF4604CFF, 0xB54738FF }; + +/* 160, 220, 255 */ +static uint32_t kBlue1[] = + { 0x4E6B7CFF, 0x74A0BAFF, 0x86B8D6FF, 0x88BBD8FF, 0x86B8D6FF, + 0x7EADC9FF, 0x7CABC6FF, 0x83B4D1FF, 0x89BDDBFF, 0x91C8E8FF, + 0x94CCEDFF, 0x98D1F2FF, 0x99D3F4FF, 0x719CB5FF }; + +/* 120, 190, 255 */ +static uint32_t kBlue2[] = + { 0x3A5D7CFF, 0x578ABAFF, 0x649FD6FF, 0x66A1D8FF, 0x649FD6FF, + 0x5E96C9FF, 0x5D94C6FF, 0x629BD1FF, 0x67A3DBFF, 0x6DACE8FF, + 0x6FB0EDFF, 0x72B4F2FF, 0x73B6F4FF, 0x5586B5FF }; + +/* 80, 160, 255 */ +static uint32_t kBlue3[] = + { 0x274E7CFF, 0x3A74BAFF, 0x4386D6FF, 0x4488D8FF, 0x4386D6FF, + 0x3F7EC9FF, 0x3E7CC6FF, 0x4183D1FF, 0x4489DBFF, 0x4891E8FF, + 0x4A94EDFF, 0x4C98F2FF, 0x4C99F4FF, 0x3871B5FF }; + +/* 30, 70, 180 */ +static uint32_t kBlue4[] = + { 0x0E2258FF, 0x153383FF, 0x193A97FF, 0x193B99FF, 0x193A97FF, + 0x17378EFF, 0x17368CFF, 0x183993FF, 0x193C9AFF, 0x1B3FA3FF, + 0x1B41A7FF, 0x1C42ABFF, 0x1C43ACFF, 0x15317FFF }; + +/* 130, 130, 130 */ +static uint32_t kGray[] = + { 0x3F3F3FFF, 0x5E5E5EFF, 0x6D6D6DFF, 0x6E6E6EFF, 0x6D6D6DFF, + 0x666666FF, 0x656565FF, 0x6A6A6AFF, 0x6F6F6FFF, 0x767676FF, + 0x787878FF, 0x7B7B7BFF, 0x7C7C7CFF, 0x5C5C5CFF }; + +/* 0, 255, 0 */ +static uint32_t kGreen[] = + { 0x007C00FF, 0x00BA00FF, 0x00D600FF, 0x00D800FF, 0x00D600FF, + 0x00C900FF, 0x00C600FF, 0x00D100FF, 0x00DB00FF, 0x00E800FF, + 0x00ED00FF, 0x00F200FF, 0x00F400FF, 0x00B500FF }; + +@implementation ProgressCell + +- (id) init +{ + NSImage * bgImg; + NSSize size; + + self = [super init]; + + /* Have a NSBitmapImageRep ready to draw the progression bar */ + bgImg = [NSImage imageNamed: @"Progress.tiff"]; + fBgBmp = [[bgImg representations] objectAtIndex: 0]; + size = [bgImg size]; + fImg = [[NSImage alloc] initWithSize: size]; + fBmp = [[NSBitmapImageRep alloc] + initWithBitmapDataPlanes: NULL pixelsWide: size.width + pixelsHigh: size.height bitsPerSample: 8 samplesPerPixel: 4 + hasAlpha: YES isPlanar: NO + colorSpaceName: NSCalibratedRGBColorSpace + bytesPerRow: 0 bitsPerPixel: 0]; + + [fImg addRepresentation: fBmp]; + + return self; +} + +- (void) setStat: (tr_stat_t *) stat +{ + int i; + + fStat = stat; + + fProgressString = [NSString stringWithFormat: + @"%.2f %%", 100.0 * fStat->progress]; + fDlString = [NSString stringWithFormat: + @"DL: %.2f KB/s", fStat->rateDownload]; + fUlString = [NSString stringWithFormat: + @"UL: %.2f KB/s", fStat->rateUpload]; + + for( i = 0; i < [fImg size].height; i++ ) + { + memcpy( [fBmp bitmapData] + i * [fBmp bytesPerRow], + [fBgBmp bitmapData] + i * [fBgBmp bytesPerRow], + [fImg size].width * 4 ); + } + + if( [[NSUserDefaults standardUserDefaults] + boolForKey:@"UseAdvancedBar"]) + { + [self buildAdvancedBar]; + } + else + { + [self buildSimpleBar]; + } +} + +- (void) buildSimpleBar +{ + int h, w; + uint32_t * p; + + for( h = 0; h < 14; h++ ) + { + p = (uint32_t *) ( [fBmp bitmapData] + + h * [fBmp bytesPerRow] ) + 2; + + for( w = 0; w < 120; w++ ) + { + *p = kBlue2[h]; + + if( w >= (int) ( fStat->progress * 120 ) ) + { + break; + } + + p++; + } + } +} + +- (void) buildAdvancedBar +{ + int h, w; + uint32_t * p; + + if( fStat->status & TR_STATUS_SEED ) + { + for( h = 0; h < 2; h++ ) + { + p = (uint32_t *) ( [fBmp bitmapData] + + h * [fBmp bytesPerRow] ) + 2; + } + } + + for( h = 0; h < 14; h++ ) + { + p = (uint32_t *) ( [fBmp bitmapData] + + h * [fBmp bytesPerRow] ) + 2; + + for( w = 0; w < 120; w++ ) + { + if( fStat->status & TR_STATUS_SEED ) + { + *p = kGreen[h]; + } + else + { + /* Download is not finished yet */ + if( h < 2 ) + { + /* First two lines: dark blue to show progression */ + *p = kBlue4[h]; + + if( w >= (int) ( fStat->progress * 120 ) ) + { + break; + } + } + else + { + /* Lines 2 to X: blue or grey depending on whether + we have the piece or not */ + if( fStat->pieces[w] < 0 ) + { + *p = kGray[h]; + } + else if( fStat->pieces[w] < 1 ) + { + *p = kRed[h]; + } + else if( fStat->pieces[w] < 2 ) + { + *p = kBlue1[h]; + } + else if( fStat->pieces[w] < 3 ) + { + *p = kBlue2[h]; + } + else + { + *p = kBlue3[h]; + } + } + } + + p++; + } + } +} + +- (void) drawWithFrame: (NSRect) cellFrame inView: (NSView *) view +{ + if( ![view lockFocusIfCanDraw] ) + { + return; + } + + NSMutableDictionary * attributes; + NSPoint pen = cellFrame.origin; + + attributes = [NSMutableDictionary dictionaryWithCapacity: 1]; + [attributes setObject: [NSFont messageFontOfSize:12.0] + forKey: NSFontAttributeName]; + + pen.x += 5; pen.y += 5; + + pen.y += [fImg size].height; + [fImg compositeToPoint: pen operation: NSCompositeSourceOver]; + pen.y -= [fImg size].height; + + [attributes setObject: [NSFont messageFontOfSize:10.0] + forKey: NSFontAttributeName]; + + pen.x += 5; pen.y += 20; + [fDlString drawAtPoint: pen withAttributes: attributes]; + + pen.x += 0; pen.y += 15; + [fUlString drawAtPoint: pen withAttributes: attributes]; + + [view unlockFocus]; +} + +@end diff --git a/macosx/Transmission.xcodeproj/project.pbxproj b/macosx/Transmission.xcodeproj/project.pbxproj new file mode 100644 index 000000000..cf0abcbfb --- /dev/null +++ b/macosx/Transmission.xcodeproj/project.pbxproj @@ -0,0 +1,386 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 42; + objects = { + +/* Begin PBXBuildFile section */ + 4D043A7F090AE979009FEDA8 /* TransmissionDocument.icns in Resources */ = {isa = PBXBuildFile; fileRef = 4D043A7E090AE979009FEDA8 /* TransmissionDocument.icns */; }; + 4D096C12089FB4E20091B166 /* NameCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D096C0F089FB4E20091B166 /* NameCell.m */; }; + 4D096C13089FB4E20091B166 /* ProgressCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D096C11089FB4E20091B166 /* ProgressCell.m */; }; + 4D118E1A08CB46B20033958F /* PrefsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D118E1908CB46B20033958F /* PrefsController.m */; }; + 4D2784370905709500687951 /* Transmission.icns in Resources */ = {isa = PBXBuildFile; fileRef = 4D2784360905709500687951 /* Transmission.icns */; }; + 4D3EA0AA08AE13C600EA10C2 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D3EA0A908AE13C600EA10C2 /* IOKit.framework */; }; + 4D6DAAC6090CE00500F43C22 /* RevealOff.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 4D6DAAC4090CE00500F43C22 /* RevealOff.tiff */; }; + 4D6DAAC7090CE00500F43C22 /* RevealOn.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 4D6DAAC5090CE00500F43C22 /* RevealOn.tiff */; }; + 4D813EB508AA43AC00191DB4 /* Progress.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 4D813EB408AA43AC00191DB4 /* Progress.tiff */; }; + 4DF0C5AB0899190500DD8943 /* Controller.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DF0C5A90899190500DD8943 /* Controller.m */; }; + 4DF0C5AE08991C1600DD8943 /* libtransmission.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DF0C5AD08991C1600DD8943 /* libtransmission.a */; }; + 4DF7500C08A103AD007B0D70 /* Open.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 4DF7500708A103AD007B0D70 /* Open.tiff */; }; + 4DF7500D08A103AD007B0D70 /* Info.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 4DF7500808A103AD007B0D70 /* Info.tiff */; }; + 4DF7500E08A103AD007B0D70 /* Remove.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 4DF7500908A103AD007B0D70 /* Remove.tiff */; }; + 4DF7500F08A103AD007B0D70 /* Resume.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 4DF7500A08A103AD007B0D70 /* Resume.tiff */; }; + 4DF7501008A103AD007B0D70 /* Stop.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 4DF7500B08A103AD007B0D70 /* Stop.tiff */; }; + 8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */ = {isa = PBXBuildFile; fileRef = 29B97318FDCFA39411CA2CEA /* MainMenu.nib */; }; + 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; + 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; + 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXBuildStyle section */ + 4A9504CCFFE6A4B311CA0CBA /* Debug */ = { + isa = PBXBuildStyle; + buildSettings = { + COPY_PHASE_STRIP = NO; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + PREBINDING = NO; + ZERO_LINK = YES; + }; + name = Debug; + }; + 4A9504CDFFE6A4B311CA0CBA /* Release */ = { + isa = PBXBuildStyle; + buildSettings = { + COPY_PHASE_STRIP = YES; + GCC_ENABLE_FIX_AND_CONTINUE = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + PREBINDING = NO; + ZERO_LINK = NO; + }; + name = Release; + }; +/* End PBXBuildStyle section */ + +/* Begin PBXFileReference section */ + 089C165DFE840E0CC02AAC07 /* English */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; name = English; path = English.lproj/InfoPlist.strings; sourceTree = ""; }; + 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; + 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; + 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 29B97319FDCFA39411CA2CEA /* English */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = English; path = English.lproj/MainMenu.nib; sourceTree = ""; }; + 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; + 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; + 32CA4F630368D1EE00C91783 /* Transmission_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Transmission_Prefix.pch; sourceTree = ""; }; + 4D043A7E090AE979009FEDA8 /* TransmissionDocument.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; name = TransmissionDocument.icns; path = Images/TransmissionDocument.icns; sourceTree = ""; }; + 4D096C0E089FB4E20091B166 /* NameCell.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = NameCell.h; sourceTree = ""; }; + 4D096C0F089FB4E20091B166 /* NameCell.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = NameCell.m; sourceTree = ""; }; + 4D096C10089FB4E20091B166 /* ProgressCell.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = ProgressCell.h; sourceTree = ""; }; + 4D096C11089FB4E20091B166 /* ProgressCell.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = ProgressCell.m; sourceTree = ""; }; + 4D118E1808CB46B20033958F /* PrefsController.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = PrefsController.h; sourceTree = ""; }; + 4D118E1908CB46B20033958F /* PrefsController.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = PrefsController.m; sourceTree = ""; }; + 4D2784360905709500687951 /* Transmission.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; name = Transmission.icns; path = Images/Transmission.icns; sourceTree = ""; }; + 4D3EA0A908AE13C600EA10C2 /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = /System/Library/Frameworks/IOKit.framework; sourceTree = ""; }; + 4D6DAAC4090CE00500F43C22 /* RevealOff.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = RevealOff.tiff; path = Images/RevealOff.tiff; sourceTree = ""; }; + 4D6DAAC5090CE00500F43C22 /* RevealOn.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = RevealOn.tiff; path = Images/RevealOn.tiff; sourceTree = ""; }; + 4D813EB408AA43AC00191DB4 /* Progress.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = Progress.tiff; path = Images/Progress.tiff; sourceTree = ""; }; + 4DF0C5A90899190500DD8943 /* Controller.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = Controller.m; sourceTree = ""; }; + 4DF0C5AA0899190500DD8943 /* Controller.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = Controller.h; sourceTree = ""; }; + 4DF0C5AD08991C1600DD8943 /* libtransmission.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libtransmission.a; path = ../libtransmission/libtransmission.a; sourceTree = SOURCE_ROOT; }; + 4DF7500708A103AD007B0D70 /* Open.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = Open.tiff; path = Images/Open.tiff; sourceTree = ""; }; + 4DF7500808A103AD007B0D70 /* Info.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = Info.tiff; path = Images/Info.tiff; sourceTree = ""; }; + 4DF7500908A103AD007B0D70 /* Remove.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = Remove.tiff; path = Images/Remove.tiff; sourceTree = ""; }; + 4DF7500A08A103AD007B0D70 /* Resume.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = Resume.tiff; path = Images/Resume.tiff; sourceTree = ""; }; + 4DF7500B08A103AD007B0D70 /* Stop.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = Stop.tiff; path = Images/Stop.tiff; sourceTree = ""; }; + 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 8D1107320486CEB800E47090 /* Transmission.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Transmission.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8D11072E0486CEB800E47090 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */, + 4DF0C5AE08991C1600DD8943 /* libtransmission.a in Frameworks */, + 4D3EA0AA08AE13C600EA10C2 /* IOKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 080E96DDFE201D6D7F000001 /* Classes */ = { + isa = PBXGroup; + children = ( + 4D096C0E089FB4E20091B166 /* NameCell.h */, + 4D096C0F089FB4E20091B166 /* NameCell.m */, + 4D096C10089FB4E20091B166 /* ProgressCell.h */, + 4D096C11089FB4E20091B166 /* ProgressCell.m */, + 4DF0C5A90899190500DD8943 /* Controller.m */, + 4DF0C5AA0899190500DD8943 /* Controller.h */, + 4D118E1808CB46B20033958F /* PrefsController.h */, + 4D118E1908CB46B20033958F /* PrefsController.m */, + ); + name = Classes; + sourceTree = ""; + }; + 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = { + isa = PBXGroup; + children = ( + 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */, + ); + name = "Linked Frameworks"; + sourceTree = ""; + }; + 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */ = { + isa = PBXGroup; + children = ( + 4D3EA0A908AE13C600EA10C2 /* IOKit.framework */, + 29B97324FDCFA39411CA2CEA /* AppKit.framework */, + 13E42FB307B3F0F600E4EEF1 /* CoreData.framework */, + 29B97325FDCFA39411CA2CEA /* Foundation.framework */, + ); + name = "Other Frameworks"; + sourceTree = ""; + }; + 19C28FACFE9D520D11CA2CBB /* Products */ = { + isa = PBXGroup; + children = ( + 8D1107320486CEB800E47090 /* Transmission.app */, + ); + name = Products; + sourceTree = ""; + }; + 29B97314FDCFA39411CA2CEA /* Transmission */ = { + isa = PBXGroup; + children = ( + 4DF0C5AD08991C1600DD8943 /* libtransmission.a */, + 080E96DDFE201D6D7F000001 /* Classes */, + 29B97315FDCFA39411CA2CEA /* Other Sources */, + 29B97317FDCFA39411CA2CEA /* Resources */, + 29B97323FDCFA39411CA2CEA /* Frameworks */, + 19C28FACFE9D520D11CA2CBB /* Products */, + ); + name = Transmission; + sourceTree = ""; + }; + 29B97315FDCFA39411CA2CEA /* Other Sources */ = { + isa = PBXGroup; + children = ( + 32CA4F630368D1EE00C91783 /* Transmission_Prefix.pch */, + 29B97316FDCFA39411CA2CEA /* main.m */, + ); + name = "Other Sources"; + sourceTree = ""; + }; + 29B97317FDCFA39411CA2CEA /* Resources */ = { + isa = PBXGroup; + children = ( + 4D2784360905709500687951 /* Transmission.icns */, + 4D043A7E090AE979009FEDA8 /* TransmissionDocument.icns */, + 4DF7500808A103AD007B0D70 /* Info.tiff */, + 4D813EB408AA43AC00191DB4 /* Progress.tiff */, + 4DF7500708A103AD007B0D70 /* Open.tiff */, + 4DF7500908A103AD007B0D70 /* Remove.tiff */, + 4DF7500A08A103AD007B0D70 /* Resume.tiff */, + 4DF7500B08A103AD007B0D70 /* Stop.tiff */, + 4D6DAAC4090CE00500F43C22 /* RevealOff.tiff */, + 4D6DAAC5090CE00500F43C22 /* RevealOn.tiff */, + 8D1107310486CEB800E47090 /* Info.plist */, + 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */, + 29B97318FDCFA39411CA2CEA /* MainMenu.nib */, + ); + name = Resources; + sourceTree = ""; + }; + 29B97323FDCFA39411CA2CEA /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */, + 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8D1107260486CEB800E47090 /* Transmission */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4DF0C596089918A300DD8943 /* Build configuration list for PBXNativeTarget "Transmission" */; + buildPhases = ( + 8D1107290486CEB800E47090 /* Resources */, + 8D11072C0486CEB800E47090 /* Sources */, + 8D11072E0486CEB800E47090 /* Frameworks */, + ); + buildRules = ( + ); + buildSettings = { + GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = Transmission_Prefix.pch; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Applications"; + PRODUCT_NAME = Transmission; + WRAPPER_EXTENSION = app; + }; + dependencies = ( + ); + name = Transmission; + productInstallPath = "$(HOME)/Applications"; + productName = Transmission; + productReference = 8D1107320486CEB800E47090 /* Transmission.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 29B97313FDCFA39411CA2CEA /* Project object */ = { + isa = PBXProject; + buildConfigurationList = 4DF0C59A089918A300DD8943 /* Build configuration list for PBXProject "Transmission" */; + buildSettings = { + }; + buildStyles = ( + 4A9504CCFFE6A4B311CA0CBA /* Debug */, + 4A9504CDFFE6A4B311CA0CBA /* Release */, + ); + hasScannedForEncodings = 1; + mainGroup = 29B97314FDCFA39411CA2CEA /* Transmission */; + projectDirPath = ""; + targets = ( + 8D1107260486CEB800E47090 /* Transmission */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8D1107290486CEB800E47090 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D11072A0486CEB800E47090 /* MainMenu.nib in Resources */, + 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */, + 4DF7500C08A103AD007B0D70 /* Open.tiff in Resources */, + 4DF7500D08A103AD007B0D70 /* Info.tiff in Resources */, + 4DF7500E08A103AD007B0D70 /* Remove.tiff in Resources */, + 4DF7500F08A103AD007B0D70 /* Resume.tiff in Resources */, + 4DF7501008A103AD007B0D70 /* Stop.tiff in Resources */, + 4D813EB508AA43AC00191DB4 /* Progress.tiff in Resources */, + 4D2784370905709500687951 /* Transmission.icns in Resources */, + 4D043A7F090AE979009FEDA8 /* TransmissionDocument.icns in Resources */, + 4D6DAAC6090CE00500F43C22 /* RevealOff.tiff in Resources */, + 4D6DAAC7090CE00500F43C22 /* RevealOn.tiff in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8D11072C0486CEB800E47090 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D11072D0486CEB800E47090 /* main.m in Sources */, + 4DF0C5AB0899190500DD8943 /* Controller.m in Sources */, + 4D096C12089FB4E20091B166 /* NameCell.m in Sources */, + 4D096C13089FB4E20091B166 /* ProgressCell.m in Sources */, + 4D118E1A08CB46B20033958F /* PrefsController.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 089C165DFE840E0CC02AAC07 /* English */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 29B97318FDCFA39411CA2CEA /* MainMenu.nib */ = { + isa = PBXVariantGroup; + children = ( + 29B97319FDCFA39411CA2CEA /* English */, + ); + name = MainMenu.nib; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 4DF0C599089918A300DD8943 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + ppc, + i386, + ); + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; + GCC_OPTIMIZATION_LEVEL = 3; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = Transmission_Prefix.pch; + GCC_TREAT_NONCONFORMANT_CODE_ERRORS_AS_WARNINGS = NO; + GCC_UNROLL_LOOPS = YES; + GCC_WARN_ABOUT_MISSING_NEWLINE = YES; + GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_CHECK_SWITCH_STATEMENTS = YES; + GCC_WARN_EFFECTIVE_CPLUSPLUS_VIOLATIONS = YES; + GCC_WARN_HIDDEN_VIRTUAL_FUNCTIONS = YES; + GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; + GCC_WARN_MISSING_PARENTHESES = YES; + GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; + GCC_WARN_SHADOW = YES; + GCC_WARN_SIGN_COMPARE = YES; + GCC_WARN_TYPECHECK_CALLS_TO_PRINTF = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNKNOWN_PRAGMAS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_LABEL = YES; + GCC_WARN_UNUSED_PARAMETER = NO; + GCC_WARN_UNUSED_VALUE = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = "\"$(SRCROOT)/../libtransmission\""; + INFOPLIST_FILE = Info.plist; + INSTALL_PATH = "$(HOME)/Applications"; + LIBRARY_SEARCH_PATHS = "\"$(SRCROOT)/../libtransmission\""; + OTHER_LDFLAGS = "-lcrypto"; + PRODUCT_NAME = Transmission; + WRAPPER_EXTENSION = app; + }; + name = Debug; + }; + 4DF0C59D089918A300DD8943 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + ppc, + i386, + ); + COPY_PHASE_STRIP = NO; + MACOSX_DEPLOYMENT_TARGET = 10.3; + SDKROOT = /Developer/SDKs/MacOSX10.4u.sdk; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 4DF0C596089918A300DD8943 /* Build configuration list for PBXNativeTarget "Transmission" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4DF0C599089918A300DD8943 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + 4DF0C59A089918A300DD8943 /* Build configuration list for PBXProject "Transmission" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4DF0C59D089918A300DD8943 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 29B97313FDCFA39411CA2CEA /* Project object */; +} diff --git a/macosx/Transmission_Prefix.pch b/macosx/Transmission_Prefix.pch new file mode 100644 index 000000000..aabef477d --- /dev/null +++ b/macosx/Transmission_Prefix.pch @@ -0,0 +1,3 @@ +#ifdef __OBJC__ + #import +#endif diff --git a/macosx/Utils.h b/macosx/Utils.h new file mode 100644 index 000000000..a7212df7d --- /dev/null +++ b/macosx/Utils.h @@ -0,0 +1,74 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +static NSString * stringForFileSize( uint64_t size ) +{ + if( size < 1024 ) + { + return [NSString stringWithFormat: @"%lld bytes", size]; + } + if( size < 1048576 ) + { + return [NSString stringWithFormat: @"%lld.%lld KB", + size / 1024, ( size % 1024 ) / 103]; + } + if( size < 1073741824 ) + { + return [NSString stringWithFormat: @"%lld.%lld MB", + size / 1048576, ( size % 1048576 ) / 104858]; + } + return [NSString stringWithFormat: @"%lld.%lld GB", + size / 1073741824, ( size % 1073741824 ) / 107374183]; +} + +static float widthForString( NSString * string, float fontSize ) +{ + NSMutableDictionary * attributes = + [NSMutableDictionary dictionaryWithCapacity: 1]; + [attributes setObject: [NSFont messageFontOfSize: fontSize] + forKey: NSFontAttributeName]; + + return [string sizeWithAttributes: attributes].width; +} + +static NSString * stringFittingInWidth( char * string, float width, + float fontSize ) +{ + NSString * nsString = NULL; + char * foo = strdup( string ); + int i; + + for( i = strlen( string ); i > 0; i-- ) + { + foo[i] = '\0'; + nsString = [NSString stringWithFormat: @"%s%@", + foo, ( i - strlen( string ) ? [NSString + stringWithUTF8String:"\xE2\x80\xA6"] : @"" )]; + + if( widthForString( nsString, fontSize ) <= width ) + { + break; + } + } + free( foo ); + return nsString; +} diff --git a/macosx/main.m b/macosx/main.m new file mode 100644 index 000000000..874d94b40 --- /dev/null +++ b/macosx/main.m @@ -0,0 +1,36 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include + +int main( int argc, char ** argv ) +{ + if( argc > 1 && !strncmp( argv[1], "-v", 2 ) ) + { + char * env; + int debug = atoi( &argv[1][2] ); + asprintf( &env, "TR_DEBUG=%d", debug ); + putenv( env ); + free( env ); + } + return NSApplicationMain( argc, (const char **) argv ); +} diff --git a/transmissioncli.c b/transmissioncli.c new file mode 100644 index 000000000..5f01b197e --- /dev/null +++ b/transmissioncli.c @@ -0,0 +1,297 @@ +/****************************************************************************** + * Copyright (c) 2005 Eric Petit + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + *****************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include + +#define USAGE \ +"Usage: %s [options] file.torrent [options]\n\n" \ +"Options:\n" \ +" -h, --help Print this help and exit\n" \ +" -i, --info Print metainfo and exit\n" \ +" -s, --scrape Print counts of seeders/leechers and exit\n" \ +" -v, --verbose Verbose level (0 to 2, default = 0)\n" \ +" -p, --port Port we should listen on (default = 9090)\n" \ +" -u, --upload Maximum upload rate (-1 = no limit, default = 20)\n" + +static int showHelp = 0; +static int showInfo = 0; +static int showScrape = 0; +static int verboseLevel = 0; +static int bindPort = 9090; +static int uploadLimit = 20; +static char * torrentPath = NULL; +static volatile char mustDie = 0; + +static int parseCommandLine ( int argc, char ** argv ); +static void sigHandler ( int signal ); + +int main( int argc, char ** argv ) +{ + int i, count; + tr_handle_t * h; + tr_stat_t * s; + + printf( "Transmission %s - http://transmission.m0k.org/\n\n", + VERSION_STRING ); + + /* Get options */ + if( parseCommandLine( argc, argv ) ) + { + printf( USAGE, argv[0] ); + return 1; + } + + if( showHelp ) + { + printf( USAGE, argv[0] ); + return 0; + } + + if( verboseLevel < 0 ) + { + verboseLevel = 0; + } + else if( verboseLevel > 9 ) + { + verboseLevel = 9; + } + if( verboseLevel ) + { + static char env[11]; + sprintf( env, "TR_DEBUG=%d", verboseLevel ); + putenv( env ); + } + + if( bindPort < 1 || bindPort > 65535 ) + { + printf( "Invalid port '%d'\n", bindPort ); + return 1; + } + + /* Initialize libtransmission */ + h = tr_init(); + + /* Open and parse torrent file */ + if( tr_torrentInit( h, torrentPath ) ) + { + printf( "Failed opening torrent file `%s'\n", torrentPath ); + goto failed; + } + + if( showInfo ) + { + tr_info_t * info; + + count = tr_torrentStat( h, &s ); + info = &s[0].info; + + /* Print torrent info (quite à la btshowmetainfo) */ + printf( "hash: " ); + for( i = 0; i < SHA_DIGEST_LENGTH; i++ ) + { + printf( "%02x", info->hash[i] ); + } + printf( "\n" ); + printf( "tracker: %s:%d\n", + info->trackerAddress, info->trackerPort ); + printf( "announce: %s\n", info->trackerAnnounce ); + printf( "size: %lld (%lld * %d + %lld)\n", + info->totalSize, info->totalSize / info->pieceSize, + info->pieceSize, info->totalSize % info->pieceSize ); + printf( "file(s):\n" ); + for( i = 0; i < info->fileCount; i++ ) + { + printf( " %s (%lld)\n", info->files[i].name, + info->files[i].length ); + } + + free( s ); + goto cleanup; + } + + if( showScrape ) + { + int seeders, leechers; + + if( tr_torrentScrape( h, 0, &seeders, &leechers ) ) + { + printf( "Scrape failed.\n" ); + } + else + { + printf( "%d seeder(s), %d leecher(s).\n", seeders, leechers ); + } + + goto cleanup; + } + + signal( SIGINT, sigHandler ); + + tr_setBindPort( h, bindPort ); + tr_setUploadLimit( h, uploadLimit ); + + tr_torrentSetFolder( h, 0, "." ); + tr_torrentStart( h, 0 ); + + while( !mustDie ) + { + char string[80]; + int chars = 0; + + sleep( 1 ); + + count = tr_torrentStat( h, &s ); + + if( s[0].status & TR_STATUS_CHECK ) + { + chars = snprintf( string, 80, + "Checking files... %.2f %%", 100.0 * s[0].progress ); + } + else if( s[0].status & TR_STATUS_DOWNLOAD ) + { + chars = snprintf( string, 80, + "Progress: %.2f %%, %d peer%s, dl from %d (%.2f kbps), " + "ul to %d (%.2f kbps)", 100.0 * s[0].progress, + s[0].peersTotal, ( s[0].peersTotal == 1 ) ? "" : "s", + s[0].peersUploading, s[0].rateDownload, + s[0].peersDownloading, s[0].rateUpload ); + } + else if( s[0].status & TR_STATUS_SEED ) + { + chars = snprintf( string, 80, + "Seeding, uploading to %d of %d peer(s), %.2f kbps", + s[0].peersDownloading, s[0].peersTotal, + s[0].rateUpload ); + } + memset( &string[chars], ' ', 79 - chars ); + string[79] = '\0'; + fprintf( stderr, "\r%s", string ); + + if( s[0].status & TR_TRACKER_ERROR ) + { + fprintf( stderr, "\n%s\n", s[0].error ); + } + else if( verboseLevel > 0 ) + { + fprintf( stderr, "\n" ); + } + + free( s ); + } + fprintf( stderr, "\n" ); + + /* Try for 5 seconds to notice the tracker that we are leaving */ + tr_torrentStop( h, 0 ); + for( i = 0; i < 10; i++ ) + { + count = tr_torrentStat( h, &s ); + if( s[0].status & TR_STATUS_PAUSE ) + { + /* The 'stopped' message was sent */ + free( s ); + break; + } + free( s ); + usleep( 500000 ); + } + +cleanup: + tr_torrentClose( h, 0 ); + +failed: + tr_close( h ); + + return 0; +} + +static int parseCommandLine( int argc, char ** argv ) +{ + for( ;; ) + { + static struct option long_options[] = + { { "help", no_argument, NULL, 'h' }, + { "info", no_argument, NULL, 'i' }, + { "scrape", no_argument, NULL, 's' }, + { "verbose", required_argument, NULL, 'v' }, + { "port", required_argument, NULL, 'p' }, + { "upload", required_argument, NULL, 'u' }, + { 0, 0, 0, 0 } }; + + int c, optind = 0; + c = getopt_long( argc, argv, "hisv:p:u:", long_options, &optind ); + if( c < 0 ) + { + break; + } + switch( c ) + { + case 'h': + showHelp = 1; + break; + case 'i': + showInfo = 1; + break; + case 's': + showScrape = 1; + break; + case 'v': + verboseLevel = atoi( optarg ); + break; + case 'p': + bindPort = atoi( optarg ); + break; + case 'u': + uploadLimit = atoi( optarg ); + break; + default: + return 1; + } + } + + if( optind > argc - 1 ) + { + return !showHelp; + } + + torrentPath = argv[optind]; + + return 0; +} + +static void sigHandler( int signal ) +{ + switch( signal ) + { + case SIGINT: + mustDie = 1; + break; + + default: + break; + } +}