feat(torrents): support tracker exclusion

This commit is contained in:
NimBold
2026-08-02 01:19:18 +03:30
parent ab9f0507d0
commit a46a64994d
20 changed files with 295 additions and 32 deletions
+1
View File
@@ -66,6 +66,7 @@ export interface AddDownloadDraftRow {
torrentPeerSpeedLimit?: string;
torrentCheckIntegrity?: boolean;
torrentTrackers?: string;
torrentExcludeTrackers?: string;
}
/**
+9
View File
@@ -6,6 +6,7 @@ import {
downloadMediaKindsMatch,
MAX_DOWNLOAD_FILENAME_BYTES,
canonicalizeDownloadFileName,
isValidTorrentExcludeTrackerList,
isValidTorrentTrackerList,
redactDownloadForPersistence,
resolveDownloadConnections
@@ -66,6 +67,14 @@ describe('Torrent tracker input validation', () => {
expect(isValidTorrentTrackerList('https://tracker.example/announce,')).toBe(false);
expect(isValidTorrentTrackerList(Array.from({ length: 65 }, (_, index) => `https://tracker${index}.example/announce`).join('\n'))).toBe(false);
});
it('accepts wildcard exclusions but rejects ambiguous mixtures', () => {
expect(isValidTorrentExcludeTrackerList('*')).toBe(true);
expect(isValidTorrentExcludeTrackerList('https://tracker.example/announce\nudp://tracker.example:6969/announce')).toBe(true);
expect(isValidTorrentExcludeTrackerList('*,https://tracker.example/announce')).toBe(false);
expect(isValidTorrentExcludeTrackerList('https://tracker.example/announce,*')).toBe(false);
expect(isValidTorrentExcludeTrackerList('https://user:pass@tracker.example/announce')).toBe(false);
});
});
describe('download connection resolution', () => {
+15 -2
View File
@@ -127,12 +127,13 @@ export const MAX_TORRENT_STOP_TIMEOUT = 7 * 24 * 60 * 60;
* The Rust validator remains authoritative because persisted data can bypass
* this helper and the browser URL parser is not the native URL parser.
*/
export const isValidTorrentTrackerList = (value: string): boolean => {
const isValidTorrentTrackerListInternal = (value: string, allowWildcard: boolean): boolean => {
const raw = value.trim();
if (!raw) return true;
if (utf8ByteLength(raw) > MAX_TORRENT_TRACKER_BYTES) return false;
const normalized = new Set<string>();
let wildcard = false;
let serializedBytes = 0;
for (const line of raw.split(/[\r\n]/)) {
const trimmedLine = line.trim();
@@ -142,6 +143,12 @@ export const isValidTorrentTrackerList = (value: string): boolean => {
if (!token || [...token].some(character => character.charCodeAt(0) < 0x20 || character.charCodeAt(0) === 0x7f)) {
return false;
}
if (allowWildcard && token === '*') {
if (normalized.size > 0) return false;
wildcard = true;
continue;
}
if (wildcard) return false;
let parsed: URL;
try {
parsed = new URL(token);
@@ -162,9 +169,15 @@ export const isValidTorrentTrackerList = (value: string): boolean => {
if (serializedBytes > MAX_TORRENT_TRACKER_BYTES) return false;
}
}
return normalized.size > 0;
return normalized.size > 0 || wildcard;
};
export const isValidTorrentTrackerList = (value: string): boolean =>
isValidTorrentTrackerListInternal(value, false);
export const isValidTorrentExcludeTrackerList = (value: string): boolean =>
isValidTorrentTrackerListInternal(value, true);
export const initMediaDomains = async () => {
try {
const domains = await invoke('get_supported_media_domains');