tianruiming | ebb3dd0 | 2025-06-09 05:07:26 +0000 | [diff] [blame^] | 1 | package tracker; |
| 2 | |
| 3 | import java.io.File; |
| 4 | import java.nio.file.Files; |
| 5 | import java.security.MessageDigest; |
| 6 | import java.util.Map; |
| 7 | import com.dampcake.bencode.Bencode; |
| 8 | import com.dampcake.bencode.Type; |
| 9 | |
| 10 | /** |
| 11 | * Enhanced InfoHash calculator that matches qBittorrent's calculation |
| 12 | * |
| 13 | * The key issues with infoHash calculation are: |
| 14 | * 1. Bencode libraries may encode data differently |
| 15 | * 2. The original torrent's info dictionary bytes should be preserved |
| 16 | * 3. Re-encoding might change the binary representation |
| 17 | */ |
| 18 | public class EnhancedInfoHashCalculator { |
| 19 | |
| 20 | /** |
| 21 | * Calculate infoHash by extracting the original info dictionary bytes |
| 22 | * from the torrent file, rather than re-encoding the parsed data |
| 23 | */ |
| 24 | public static String calculateInfoHash(File torrentFile) throws Exception { |
| 25 | byte[] torrentData = Files.readAllBytes(torrentFile.toPath()); |
| 26 | |
| 27 | // Find the info dictionary in the raw torrent data |
| 28 | // Look for the pattern "4:info" which indicates the start of the info dictionary |
| 29 | int infoStart = findInfoDictionary(torrentData); |
| 30 | if (infoStart == -1) { |
| 31 | throw new Exception("Could not find info dictionary in torrent file"); |
| 32 | } |
| 33 | |
| 34 | // Extract the info dictionary bytes directly from the original torrent |
| 35 | byte[] infoBytes = extractInfoBytes(torrentData, infoStart); |
| 36 | |
| 37 | // Calculate SHA1 hash |
| 38 | MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); |
| 39 | byte[] digest = sha1.digest(infoBytes); |
| 40 | |
| 41 | // Convert to hex string |
| 42 | StringBuilder sb = new StringBuilder(); |
| 43 | for (byte b : digest) { |
| 44 | sb.append(String.format("%02x", b & 0xff)); |
| 45 | } |
| 46 | |
| 47 | return sb.toString(); |
| 48 | } |
| 49 | |
| 50 | /** |
| 51 | * Find the position of "4:info" in the torrent data |
| 52 | */ |
| 53 | private static int findInfoDictionary(byte[] data) { |
| 54 | byte[] pattern = "4:info".getBytes(); |
| 55 | |
| 56 | for (int i = 0; i <= data.length - pattern.length; i++) { |
| 57 | boolean found = true; |
| 58 | for (int j = 0; j < pattern.length; j++) { |
| 59 | if (data[i + j] != pattern[j]) { |
| 60 | found = false; |
| 61 | break; |
| 62 | } |
| 63 | } |
| 64 | if (found) { |
| 65 | return i; |
| 66 | } |
| 67 | } |
| 68 | return -1; |
| 69 | } |
| 70 | |
| 71 | /** |
| 72 | * Extract the info dictionary bytes from the original torrent data |
| 73 | */ |
| 74 | private static byte[] extractInfoBytes(byte[] data, int infoStart) throws Exception { |
| 75 | // Skip "4:info" to get to the actual dictionary content |
| 76 | int dictStart = infoStart + 6; // "4:info".length() |
| 77 | |
| 78 | if (dictStart >= data.length || data[dictStart] != 'd') { |
| 79 | throw new Exception("Invalid info dictionary format"); |
| 80 | } |
| 81 | |
| 82 | // Find the matching 'e' that closes the info dictionary |
| 83 | int dictEnd = findMatchingEnd(data, dictStart); |
| 84 | if (dictEnd == -1) { |
| 85 | throw new Exception("Could not find end of info dictionary"); |
| 86 | } |
| 87 | |
| 88 | // Extract the info dictionary bytes (including 'd' and 'e') |
| 89 | int length = dictEnd - dictStart + 1; |
| 90 | byte[] infoBytes = new byte[length]; |
| 91 | System.arraycopy(data, dictStart, infoBytes, 0, length); |
| 92 | |
| 93 | return infoBytes; |
| 94 | } |
| 95 | |
| 96 | /** |
| 97 | * Find the matching 'e' for a dictionary that starts with 'd' |
| 98 | */ |
| 99 | private static int findMatchingEnd(byte[] data, int start) { |
| 100 | if (start >= data.length || data[start] != 'd') { |
| 101 | return -1; |
| 102 | } |
| 103 | |
| 104 | int depth = 0; |
| 105 | for (int i = start; i < data.length; i++) { |
| 106 | if (data[i] == 'd' || data[i] == 'l') { |
| 107 | depth++; |
| 108 | } else if (data[i] == 'e') { |
| 109 | depth--; |
| 110 | if (depth == 0) { |
| 111 | return i; |
| 112 | } |
| 113 | } |
| 114 | } |
| 115 | return -1; |
| 116 | } |
| 117 | |
| 118 | /** |
| 119 | * For comparison: calculate using the re-encoding method (your original approach) |
| 120 | */ |
| 121 | public static String calculateInfoHashByReencoding(File torrentFile) throws Exception { |
| 122 | byte[] torrentData = Files.readAllBytes(torrentFile.toPath()); |
| 123 | Bencode bencode = new Bencode(); |
| 124 | |
| 125 | @SuppressWarnings("unchecked") |
| 126 | Map<String,Object> meta = bencode.decode(torrentData, Type.DICTIONARY); |
| 127 | @SuppressWarnings("unchecked") |
| 128 | Map<String,Object> info = (Map<String,Object>) meta.get("info"); |
| 129 | |
| 130 | if (info == null) { |
| 131 | throw new Exception("No info dictionary found"); |
| 132 | } |
| 133 | |
| 134 | // Re-encode the info dictionary |
| 135 | byte[] infoBytes = bencode.encode(info); |
| 136 | |
| 137 | // Calculate SHA1 |
| 138 | MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); |
| 139 | byte[] digest = sha1.digest(infoBytes); |
| 140 | |
| 141 | StringBuilder sb = new StringBuilder(); |
| 142 | for (byte b : digest) { |
| 143 | sb.append(String.format("%02x", b & 0xff)); |
| 144 | } |
| 145 | |
| 146 | return sb.toString(); |
| 147 | } |
| 148 | |
| 149 | public static void main(String[] args) { |
| 150 | if (args.length != 1) { |
| 151 | System.out.println("Usage: java EnhancedInfoHashCalculator <torrent-file>"); |
| 152 | return; |
| 153 | } |
| 154 | |
| 155 | File torrentFile = new File(args[0]); |
| 156 | if (!torrentFile.exists()) { |
| 157 | System.out.println("Torrent file not found: " + args[0]); |
| 158 | return; |
| 159 | } |
| 160 | |
| 161 | try { |
| 162 | System.out.println("=== InfoHash Calculation Comparison ==="); |
| 163 | System.out.println("File: " + torrentFile.getName()); |
| 164 | |
| 165 | String directHash = calculateInfoHash(torrentFile); |
| 166 | System.out.println("Direct extraction method: " + directHash); |
| 167 | System.out.println("Direct (uppercase): " + directHash.toUpperCase()); |
| 168 | |
| 169 | String reencodingHash = calculateInfoHashByReencoding(torrentFile); |
| 170 | System.out.println("Re-encoding method: " + reencodingHash); |
| 171 | System.out.println("Re-encoding (uppercase): " + reencodingHash.toUpperCase()); |
| 172 | |
| 173 | if (directHash.equals(reencodingHash)) { |
| 174 | System.out.println("✓ Both methods produce the same result"); |
| 175 | } else { |
| 176 | System.out.println("✗ Methods produce different results!"); |
| 177 | System.out.println("This suggests the Bencode re-encoding is changing the data"); |
| 178 | } |
| 179 | |
| 180 | } catch (Exception e) { |
| 181 | System.out.println("Error: " + e.getMessage()); |
| 182 | e.printStackTrace(); |
| 183 | } |
| 184 | } |
| 185 | } |