Jump to content

Recommended Posts

Posted (edited)

Hello,

This is my UDF working with *.torrent files.

It converts torrent file to readable and editable text structure and back to torrent.

Binary data will be encoded in base64.

Written on pure AutoIt, except base64 located

Due to limitation of this version of base64 code, code will not work on x64 compilation.

Updated to new x64 compatible code.

This code can also be used to edit utorrent/BitTorrent config files (like settings.dat), as it encoded using bencode.

Examples are included.

torrentUDF.zip

Edited by xrewndel
  • 11 months later...
Posted (edited)

Thanks for making this, I was having trouble working with torrent files in autoit until I found this thread.

I modified your _Torrent_Parse function slightly to add outputting of the infohash for a torrent file because I had a need to do this:

#include <Crypt.au3>

Func _Torrent_Parse($File)
    Local $Data, $Binary, $hFile, $Byte, $Number, $String, $Size, $infodictlevel, $recording_info_bytes=false, $infobytes
    $hFile = FileOpen($File, 16)
    If @error Then Return SetError(1, 0, '')
    While True
        $Byte = BinaryToString(FileRead($hFile, 1))
        if $recording_info_bytes then $infobytes &= $Byte
        If @error Then ExitLoop
        Switch $Byte
            Case 'd'; Dictionary ->
                $Data &= __Torrents_StringFormat('Dictionary')
                __Torrents_Level(1)
                if $string='info' then
                    $infodictlevel=__Torrents_Level()
                    $recording_info_bytes = true
                    $infobytes = $Byte
                EndIf
            Case 'l'; List ->
                $Data &= __Torrents_StringFormat('List')
                __Torrents_Level(1)
            Case 'i'; Integer
                $Number = ''
                Do
                    $Number &= $Byte
                    $Byte = BinaryToString(FileRead($hFile, 1))
                    if $recording_info_bytes then $infobytes &= $Byte
                    If @error Then ExitLoop
                Until $Byte = 'e'
                VarTrim($Number, 1)
                Switch $String
                    Case 'length', 'piece length'
                        If $Number < 2 ^ 20 Then
                            $Number &= ' (' & Round($Number / 2 ^ 10, 3) & ' KiB)'
                        ElseIf $Number >= 2 ^ 20 And $Number < 2 ^ 30 Then
                            $Number &= ' (' & Round($Number / 2 ^ 20, 3) & ' MiB)'
                        Else
                            $Number &= ' (' & Round($Number / 2 ^ 30, 3) & ' GiB)'
                        EndIf
                    Case 'creation date'
                        $Number &= ' (' & _DateAdd('s', $Number, "1970/01/01 00:00:00") & ')'
                EndSwitch
                $Data &= __Torrents_StringFormat('integer: ' & $Number)
            Case 'e'; End of element <-
                __Torrents_Level(-1)
                if __Torrents_Level()=$infodictlevel-1 then $recording_info_bytes = false
                $Data &= __Torrents_StringFormat('End')
            Case '0' To '9'; String's size
                $Size &= $Byte
            Case ':'; String
                $String = BinaryToString(FileRead($hFile, $Size))
                if $recording_info_bytes then $infobytes &= $String
                $Size = ''
                If StringInStr($String, @LF) or StringInStr($String, @CR) Then
                    $Data &= __Torrents_StringFormat('binary: ' & _Base64Encode(StringToBinary($String), 0))
                Else
                    $Data &= __Torrents_StringFormat('string: ' & $String)
                EndIf
            Case Else; Error
                Return SetError(2, 0, '')
        EndSwitch
    WEnd
    Local $bHash = _Crypt_HashData($infobytes, $CALG_SHA1)
    $Data &= @CRLF& 'Infohash: '&StringTrimLeft($bHash,2)
    Return StringAddCR($Data)
EndFunc   ;==>_Torrent_Parse
Edited by garbb
  • 12 years later...
Posted

..it lives ! :lol:
This a good thread for the post :) 

Took the code for torrentcheck, AutoIt-tize it, and added what I wanted.

#pragma compile(Console, True)
#pragma compile(Icon, "")
#AutoIt3Wrapper_AU3Check_Parameters=-q -d -w 1 -w 2 -w 3 -w 4 -w 5 -w 6 -w 7
#include <Crypt.au3>
#include <File.au3>

; https://www.autoitscript.com/forum/topic/145656-torrent-bencode-udf/

; Global configuration flags
Global $g_sTorrentFile = ""
Global $g_sContentPath = ""
Global $g_bSingleMismatch = False
Global $g_bHideProgress = False
Global $g_bShowHashes = False
Global $g_sNumberFormat = "" ; "" = raw, "c" = comma, "d" = dot
Global $g_bSHA1Mode = False
Global $g_sSHA1Target = ""

Func ShowUsage()
    ConsoleWrite("torrentcheck (AutoIt Edition)" & @CRLF) ; "https://github.com/Network-BEncode-inside/torrentcheck"
    ConsoleWrite("Usage: torrentcheck [torrent-file] [-t torrent-file] [-p content-path] [-n] [-h] [-c] [-d] [-sha1 [hash]]" & @CRLF)
    ConsoleWrite("Options:" & @CRLF)
    ConsoleWrite("  -t <torrent-file> Path to .torrent file" & @CRLF)
    ConsoleWrite("  -p <content-path> Path to files/folder to check" & @CRLF)
    ConsoleWrite("  -m               On Single Mismatch stop checking" & @CRLF) ; new ( not in the original source )
    ConsoleWrite("  -n               Suppress progress indicator" & @CRLF)
    ConsoleWrite("  -h               Show piece hashes" & @CRLF)
    ConsoleWrite("  -c               Comma-formatted numbers (1,000,000)" & @CRLF)
    ConsoleWrite("  -d               Dot-formatted numbers (1.000.000)" & @CRLF)
    ConsoleWrite("  -sha1 [hash]     Act as SHA1 stdin filter" & @CRLF)
EndFunc   ;==>ShowUsage

Main()
Func Main()
    ParseCommandLine()

    ; --- SHA-1 Filter Mode ---
    If $g_bSHA1Mode Then
        _Crypt_Startup()
        Local $sCalculatedHash = CalculateStdinSHA1()
        _Crypt_Shutdown()

        ConsoleWrite("SHA1: " & $sCalculatedHash & @CRLF)
        If $g_sSHA1Target <> "" Then
            If StringLower($sCalculatedHash) = StringLower($g_sSHA1Target) Then
                ConsoleWrite("SHA1 Hash Matches." & @CRLF)
                Exit (0)
            Else
                ConsoleWrite("SHA1 Hash MISMATCH!" & @CRLF)
                Exit (1)
            EndIf
        EndIf
        Exit (0)
    EndIf

    ; --- Torrent Mode ---
    If $g_sTorrentFile = "" Then
        ShowUsage()
        Exit (1)
    EndIf

    If Not FileExists($g_sTorrentFile) Then
        ConsoleWrite("Error: Torrent file not found: " & $g_sTorrentFile & @CRLF)
        Exit (1)
    EndIf

    ; Read torrent file binary
    Local $iLen, $hFile = FileOpen($g_sTorrentFile, 16) ; Read in binary mode
    If $hFile = -1 Then
        ConsoleWrite("Error: Unable to open torrent file." & @CRLF)
        Exit (1)
    EndIf
    Local $bTorrentData = FileRead($hFile)
    FileClose($hFile)

    ; Parse Bencode
    Local $iPos = 1
    Local $oRoot = Bencode_Parse($bTorrentData, $iPos)
    If @error Or Not IsObj($oRoot) Then
        ConsoleWrite("Error: Invalid or corrupted bencoded torrent file." & @CRLF)
        Exit (1)
    EndIf

    ; Extract Torrent Info
    If Not $oRoot.Exists("info") Then
        ConsoleWrite("Error: Missing 'info' dictionary in torrent file." & @CRLF)
        Exit (1)
    EndIf

    Local $oInfo = $oRoot.Item("info")
    Local $sAnnounce = $oRoot.Exists("announce") ? BinaryToString($oRoot.Item("announce")) : "N/A"
    Local $sCreatedBy = $oRoot.Exists("created by") ? BinaryToString($oRoot.Item("created by")) : "N/A"
    Local $sComment = $oRoot.Exists("comment") ? BinaryToString($oRoot.Item("comment")) : "N/A"

    Local $iPieceLength = Number($oInfo.Item("piece length"))
    Local $bPieces = $oInfo.Item("pieces")
    Local $iTotalPieceBytes = BinaryLen($bPieces)
    Local $iPieceCount = Int($iTotalPieceBytes / 20)

    Local $sTorrentName = BinaryToString($oInfo.Item("name"))
    Local $sTorrentVersion = DetectTorrentVersion($oInfo) ; <-- Add this line

    ConsoleWrite("==================================================" & @CRLF)
    ConsoleWrite("Torrent Metadata Summary" & @CRLF)
    ConsoleWrite("==================================================" & @CRLF)
    ConsoleWrite("Name:         " & $sTorrentName & @CRLF)
    ConsoleWrite("Version:      " & $sTorrentVersion & @CRLF) ; <-- Add this line
    ConsoleWrite("Tracker:      " & $sAnnounce & @CRLF)
    ConsoleWrite("Created By:   " & $sCreatedBy & @CRLF)
    ConsoleWrite("Comment:      " & $sComment & @CRLF)
    ConsoleWrite("Piece Length: " & FormatNumber($iPieceLength) & " bytes" & @CRLF)
    ConsoleWrite("Total Pieces: " & $iPieceCount & @CRLF)
    ConsoleWrite("--------------------------------------------------" & @CRLF)

    ; --- Print File Hashes ---
    If $oInfo.Exists("file tree") Then
        ConsoleWrite(@CRLF & "Expected File Hashes (BitTorrent v2 SHA-256):" & @CRLF)
        ConsoleWrite("--------------------------------------------------------------------------------" & @CRLF)
        PrintV2FileHashes($oInfo.Item("file tree"))
        ConsoleWrite("================================================================================" & @CRLF)
    Else
        ConsoleWrite(@CRLF & "Note: Individual per-file hashes are not available in BitTorrent v1 torrents." & @CRLF)
    EndIf

    ; Extract File List
    Local $sRelPath, $aFiles[0][2] ; [Path, Size]
    Local $aSubPaths, $oFileDict, $iTotalSize = 0

    If $oInfo.Exists("files") Then
        ; Multi-file torrent
        Local $aFileList = $oInfo.Item("files")
        For $i = 0 To UBound($aFileList) - 1
            $oFileDict = $aFileList[$i]
            $iLen = Number($oFileDict.Item("length"))
            $aSubPaths = $oFileDict.Item("path")

            $sRelPath = $sTorrentName
            For $j = 0 To UBound($aSubPaths) - 1
                $sRelPath &= "\" & BinaryToString($aSubPaths[$j])
            Next

            AddFileToList($aFiles, $sRelPath, $iLen)
            $iTotalSize += $iLen
        Next
    Else
        ; Single-file torrent
        $iLen = Number($oInfo.Item("length"))
        AddFileToList($aFiles, $sTorrentName, $iLen)
        $iTotalSize += $iLen
    EndIf

    ConsoleWrite("Total Size:   " & FormatNumber($iTotalSize) & " bytes (" & UBound($aFiles) & " files)" & @CRLF)
    ConsoleWrite("--------------------------------------------------" & @CRLF)

    For $i = 0 To UBound($aFiles) - 1
        ConsoleWrite(StringFormat("%-12s  %s\n", FormatNumber($aFiles[$i][1]), $aFiles[$i][0]))
    Next
    ConsoleWrite("==================================================" & @CRLF)

    ; Print piece hashes if -h specified
    If $g_bShowHashes Then
        ConsoleWrite(@CRLF & "Piece SHA-1 Hashes:" & @CRLF)
        For $p = 0 To $iPieceCount - 1
            Local $bPieceHash = BinaryMid($bPieces, ($p * 20) + 1, 20)
            ConsoleWrite(StringFormat("Piece %4d: %s\n", $p + 1, StringTrimLeft(Hex($bPieceHash), 0)))
        Next
    EndIf

    ; --- Piece Verification (-p) ---
    If $g_sContentPath = "" Then
        Exit (0)
    EndIf

    ConsoleWrite(@CRLF & "Verifying torrent piece hashes against data on disk..." & @CRLF)

    _Crypt_Startup()
    Local $bSuccess = VerifyTorrentData($aFiles, $bPieces, $iPieceLength, $iPieceCount)
    _Crypt_Shutdown()

    If $bSuccess Then
        ConsoleWrite(@CRLF & "RESULT: Torrent is GOOD. All files verified successfully!" & @CRLF)
        Exit (0)
    Else
        ConsoleWrite(@CRLF & "RESULT: Torrent verification FAILED! Hash mismatches or missing files." & @CRLF)
        Exit (1)
    EndIf
EndFunc   ;==>Main

; ==============================================================================
; Bencode Parser implementation
; ==============================================================================
Func Bencode_Parse(ByRef $bData, ByRef $iPos)
    If $iPos > BinaryLen($bData) Then Return SetError(1, 0, Null)

    Local $sChar = BinaryToString(BinaryMid($bData, $iPos, 1))

    ; Integer: i<number>e
    If $sChar = "i" Then
        $iPos += 1
        Local $iEnd = _BinaryInStr($bData, Binary("e"), $iPos) ; <-- Updated here
        If $iEnd = 0 Then Return SetError(2, 0, Null)
        Local $sNum = BinaryToString(BinaryMid($bData, $iPos, $iEnd - $iPos))
        $iPos = $iEnd + 1
        Return Number($sNum)

        ; List: l<items>e
    ElseIf $sChar = "l" Then
        $iPos += 1
        Local $aList[0]
        While $iPos <= BinaryLen($bData)
            If BinaryToString(BinaryMid($bData, $iPos, 1)) = "e" Then
                $iPos += 1
                ExitLoop
            EndIf
            Local $vItem = Bencode_Parse($bData, $iPos)
            ReDim $aList[UBound($aList) + 1]
            $aList[UBound($aList) - 1] = $vItem
        WEnd
        Return $aList

        ; Dictionary: d<key><val>...e
    ElseIf $sChar = "d" Then
        $iPos += 1
        Local $oDict = ObjCreate("Scripting.Dictionary")
        While $iPos <= BinaryLen($bData)
            If BinaryToString(BinaryMid($bData, $iPos, 1)) = "e" Then
                $iPos += 1
                ExitLoop
            EndIf
            Local $sKey = Bencode_Parse($bData, $iPos)
            If IsBinary($sKey) Then $sKey = BinaryToString($sKey)
            Local $vVal = Bencode_Parse($bData, $iPos)
            If Not $oDict.Exists($sKey) Then $oDict.Add($sKey, $vVal)
        WEnd
        Return $oDict

        ; Byte String / Binary: <length>:<content>
    ElseIf StringIsDigit($sChar) Then
        Local $iColon = _BinaryInStr($bData, Binary(":"), $iPos) ; <-- Updated here
        If $iColon = 0 Then Return SetError(3, 0, Null)
        Local $iLen = Number(BinaryToString(BinaryMid($bData, $iPos, $iColon - $iPos)))
        $iPos = $iColon + 1
        Local $bStr = BinaryMid($bData, $iPos, $iLen)
        $iPos += $iLen
        Return $bStr
    EndIf

    Return SetError(4, 0, Null)
EndFunc   ;==>Bencode_Parse

; ==============================================================================
; Torrent Piece Verification Engine
; ==============================================================================
Func VerifyTorrentData(ByRef $aFiles, $bPieces, $iPieceLength, $iPieceCount)
    Local $iFileIdx = 0
    Local $iFilePos = 0
    Local $hFile = -1
    Local $iErrors = 0

    For $p = 0 To $iPieceCount - 1
        Local $bExpectedHash = BinaryMid($bPieces, ($p * 20) + 1, 20)
        Local $bPieceBuffer = Binary("")
        Local $iBytesNeeded = $iPieceLength

        While $iBytesNeeded > 0 And $iFileIdx < UBound($aFiles)
            ; Resolve file path on disk
            If $hFile = -1 Then
                Local $sFullPath = FixPath($g_sContentPath, $aFiles[$iFileIdx][0])
                If Not FileExists($sFullPath) Then
                    ConsoleWrite(@CRLF & "Error: Missing file: " & $sFullPath & @CRLF)
                    Return False
                EndIf
                $hFile = FileOpen($sFullPath, 16)
                $iFilePos = 0
            EndIf

            Local $iFileRemain = $aFiles[$iFileIdx][1] - $iFilePos
            Local $iToRead = ($iBytesNeeded < $iFileRemain) ? $iBytesNeeded : $iFileRemain

            If $iToRead > 0 Then
                Local $bChunk = FileRead($hFile, $iToRead)
                $bPieceBuffer &= $bChunk
                $iFilePos += BinaryLen($bChunk)
                $iBytesNeeded -= BinaryLen($bChunk)
            EndIf

            ; Advance to next file if current file reached EOF
            If $iFilePos >= $aFiles[$iFileIdx][1] Then
                FileClose($hFile)
                $hFile = -1
                $iFileIdx += 1
            EndIf
        WEnd

        ; Compute SHA-1 of piece buffer
        Local $bCalculatedHash = _Crypt_HashData($bPieceBuffer, $CALG_SHA1)

        If $bCalculatedHash <> $bExpectedHash Then
            $iErrors += 1
            ConsoleWrite(StringFormat("\nPiece %4d: [FAILED] Mismatch!", $p + 1))
            If $g_bSingleMismatch Then ExitLoop
        ElseIf Not $g_bHideProgress Then
            ConsoleWrite(StringFormat("\rChecking pieces... [%d/%d]", $p + 1, $iPieceCount))
        EndIf
    Next

    If $hFile <> -1 Then FileClose($hFile)
    If Not $g_bHideProgress Then ConsoleWrite(@CRLF)

    Return ($iErrors == 0)
EndFunc   ;==>VerifyTorrentData

; ==============================================================================
; Utility & Helper Functions
; ==============================================================================
Func FixPath($sBase, $sRel)
    $sBase = StringRegExpReplace($sBase, "[\\/]+$", "")
    $sRel = StringRegExpReplace($sRel, "^[\\/]+", "")

    ; If user points directly to target single file or directory containing files
    If FileExists($sBase & "\" & $sRel) Then
        Return $sBase & "\" & $sRel
    ElseIf FileExists($sBase) And Not StringInStr(FileGetAttrib($sBase), "D") Then
        Return $sBase
    EndIf

    ; Strip root folder name if user passed target inside the root folder
    Local $iFirstSlash = StringInStr($sRel, "\")
    If $iFirstSlash > 0 Then
        Local $sStrippedRel = StringMid($sRel, $iFirstSlash + 1)
        If FileExists($sBase & "\" & $sStrippedRel) Then
            Return $sBase & "\" & $sStrippedRel
        EndIf
    EndIf

    Return $sBase & "\" & $sRel
EndFunc   ;==>FixPath

Func AddFileToList(ByRef $aFiles, $sPath, $iSize)
    Local $iCount = UBound($aFiles)
    ReDim $aFiles[$iCount + 1][2]
    $aFiles[$iCount][0] = $sPath
    $aFiles[$iCount][1] = $iSize
EndFunc   ;==>AddFileToList

Func FormatNumber($iValue)
    If $g_sNumberFormat = "" Then Return $iValue
    Local $sSep = ($g_sNumberFormat = "c") ? "," : "."
    Local $sStr = String($iValue)
    Local $sRes = ""
    Local $iLen = StringLen($sStr)

    For $i = 1 To $iLen
        $sRes = StringMid($sStr, $iLen - $i + 1, 1) & $sRes
        If Mod($i, 3) = 0 And $i < $iLen Then $sRes = $sSep & $sRes
    Next
    Return $sRes
EndFunc   ;==>FormatNumber

Func CalculateStdinSHA1()
    Local $hStdin = FileOpen("*", 16) ; STDIN binary mode
    Local $bData = Binary("")
    While True
        Local $bChunk = FileRead($hStdin, 65536)
        If @error Then ExitLoop
        $bData &= $bChunk
    WEnd
    FileClose($hStdin)
    Return StringTrimLeft(Hex(_Crypt_HashData($bData, $CALG_SHA1)), 0)
EndFunc   ;==>CalculateStdinSHA1

Func ParseCommandLine()
    For $i = 1 To $CmdLine[0]
        Switch $CmdLine[$i]
            Case "-t"
                If $i + 1 <= $CmdLine[0] Then
                    $g_sTorrentFile = $CmdLine[$i + 1]
                    $i += 1
                EndIf
            Case "-p"
                If $i + 1 <= $CmdLine[0] Then
                    $g_sContentPath = $CmdLine[$i + 1]
                    $i += 1
            EndIf
            Case "-m"
                $g_bSingleMismatch = True
            Case "-n"
                $g_bHideProgress = True
            Case "-h"
                $g_bShowHashes = True
            Case "-c"
                $g_sNumberFormat = "c"
            Case "-d"
                $g_sNumberFormat = "d"
            Case "-sha1"
                $g_bSHA1Mode = True
                If $i + 1 <= $CmdLine[0] And Not StringStartsWith($CmdLine[$i + 1], "-") Then
                    $g_sSHA1Target = $CmdLine[$i + 1]
                    $i += 1
                EndIf
            Case Else
                If $g_sTorrentFile = "" And Not StringStartsWith($CmdLine[$i], "-") Then
                    $g_sTorrentFile = $CmdLine[$i]
                EndIf
        EndSwitch
    Next
EndFunc   ;==>ParseCommandLine

Func StringStartsWith($sStr, $sPrefix)
    Return StringLeft($sStr, StringLen($sPrefix)) = $sPrefix
EndFunc   ;==>StringStartsWith

Func _BinaryInStr($bData, $bNeedle, $iStartPos = 1)
    Local $sDataHex = StringTrimLeft(String($bData), 2)
    Local $sNeedleHex = StringTrimLeft(String($bNeedle), 2)
    Local $iHexStart = ($iStartPos - 1) * 2 + 1

    Local $iPosHex = StringInStr($sDataHex, $sNeedleHex, 1, 1, $iHexStart)
    If $iPosHex = 0 Then Return 0

    Return Ceiling($iPosHex / 2)
EndFunc   ;==>_BinaryInStr

Func DetectTorrentVersion($oInfo) ; new ( not in the original source )
    Local $bHasV1Pieces = $oInfo.Exists("pieces") And (BinaryLen($oInfo.Item("pieces")) > 0)
    Local $bHasMetaV2 = $oInfo.Exists("meta version") And (Number($oInfo.Item("meta version")) == 2)
    Local $bHasFileTree = $oInfo.Exists("file tree")

    If $bHasMetaV2 Or $bHasFileTree Then
        If $bHasV1Pieces Then
            Return "Hybrid (BitTorrent v1 + v2)"
        Else
            Return "BitTorrent v2"
        EndIf
    EndIf

    Return "BitTorrent v1"
EndFunc   ;==>DetectTorrentVersion

; Recursively prints SHA-256 pieces root for files in BitTorrent v2 / Hybrid file tree ; new ( not in the original source )
Func PrintV2FileHashes($oDict, $sCurrentPath = "")
    Local $aKeys = $oDict.Keys()
    For $sKey In $aKeys
        Local $vVal = $oDict.Item($sKey)

        If $sKey == "" Then
            ; Leaf node containing file metadata
            If IsObj($vVal) And $vVal.Exists("pieces root") Then
                Local $bRootHash = $vVal.Item("pieces root")
                Local $sHexHash = StringTrimLeft(Hex($bRootHash), 0)
                ConsoleWrite(StringFormat("%-64s  %s\n", $sHexHash, $sCurrentPath))
            Else
                ConsoleWrite(StringFormat("%-64s  %s\n", "[No SHA-256 Root]", $sCurrentPath))
            EndIf
        Else
            ; Directory or file container node
            Local $sNextPath = ($sCurrentPath = "") ? $sKey : $sCurrentPath & "\" & $sKey
            If IsObj($vVal) Then
                PrintV2FileHashes($vVal, $sNextPath)
            EndIf
        EndIf
    Next
EndFunc   ;==>PrintV2FileHashes

Why?, ...because I had a faster direct download than the torrent but wanted to check if it was exactly the same and had no hash for the file. This worked to verify the file.
Limited testing other than what I wanted to do but it should fully work ( never did compile it, just run from SciTE ).

Follow the link to my code contribution ( and other things too ).
FAQ - Please Read Before Posting  image.gif.922e3a93535f431de08b31ee669cc446.gif
autoit_scripter_blue_userbar.png

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...