public static string Create(TorrentInfo torrentInfo, bool includeTrackers = true) { if (torrentInfo is null) { throw new ArgumentNullException(nameof(torrentInfo)); } int size = 60 + (includeTrackers ? 150 : 0) + (string.IsNullOrWhiteSpace(torrentInfo.DisplayName) ? 0 : torrentInfo.DisplayName.Length * 2); StringBuilder sb = new StringBuilder(size); sb.Append("magnet:?xt=urn:btih:"); Hex.Encode(torrentInfo.InfoHashV2 ?? torrentInfo.InfoHashV1, sb); if (!string.IsNullOrWhiteSpace(torrentInfo.DisplayName)) { sb.Append("&dn="); sb.Append(HttpUtility.UrlEncode(torrentInfo.DisplayName)); } if (includeTrackers) { foreach (var tracker in torrentInfo.Trackers) { sb.Append("&tr="); sb.Append(HttpUtility.UrlEncode(tracker)); } } return(sb.ToString()); }
public static bool TryParse(ReadOnlySpan <char> link, out TorrentInfo torrentInfo) { torrentInfo = null; // "magnet:?xt=urn:btih:" = 20 chars if (link.Length < 20 + 2 * 20) { return(false); } if (!link.StartsWith("magnet:?", StringComparison.OrdinalIgnoreCase)) { return(false); } link = link.Slice(8); byte[] infoHash = null; string displayName = null; List <string> trackers = new List <string>(); try { while (link.Length > 0) { int tagEndIdx = link.IndexOf('='); if (tagEndIdx < 1 || tagEndIdx > 32) { return(false); } Span <char> tag = stackalloc char[tagEndIdx * 2]; int tagLen = link.Slice(0, tagEndIdx).ToLowerInvariant(tag); ReadOnlySpan <char> roTag = tag.Slice(0, tagLen); link = link.Slice(tagEndIdx + 1); int endIndex = link.IndexOf('&'); ReadOnlySpan <char> value = endIndex == -1 ? link : link.Slice(0, endIndex); link = endIndex == -1 ? ReadOnlySpan <char> .Empty : link.Slice(endIndex + 1); if (roTag.SequenceEqual("xt")) { if (value.Length < 9 + 2 * 20) { return(false); } if (!value.StartsWith("urn:btih:", StringComparison.OrdinalIgnoreCase) && !value.StartsWith("urn:sha1:", StringComparison.OrdinalIgnoreCase)) { return(false); } if (!Hex.TryParse(value.Slice(9), out infoHash)) { return(false); } } else if (roTag.SequenceEqual("dn")) { displayName = HttpUtility.UrlDecode(value.ToString()); } else if (roTag.SequenceEqual("tr")) { trackers.Add(HttpUtility.UrlDecode(value.ToString())); } else { // ToDo - Log unknown params } } } catch { return(false); } if (infoHash is null) { return(false); } torrentInfo = new TorrentInfo() { DisplayName = displayName }; torrentInfo.Trackers.AddRange(trackers); return(true); }
public static bool TryParse(ReadOnlySpan <byte> bytes, out TorrentInfo torrent, bool strictComplianceParsing = false) { torrent = null; if (!BEncodingSerializer.TryParse(bytes, out BDictionary dictionary, strictDictionaryOrder: strictComplianceParsing)) { return(false); } if (dictionary.Count > 50) { return(false); } if (!dictionary.TryGet("info", out BDictionary info)) { return(false); } if (info.Count < 3 || info.Count > 50) { return(false); } torrent = new TorrentInfo(); if (!info.TryGet("name", out BString name) || !name.IsString) { return(false); } torrent.DisplayName = name.String; if (!info.TryGet("piece length", out BInteger bpieceLength) || !bpieceLength.TryAs(out uint pieceLength)) { return(false); } torrent.PieceLength = (PieceLength)pieceLength; if (pieceLength == 0 || !torrent.PieceLength.IsValid()) { return(false); } if (info.TryGet("pieces", out BString pieces)) { if (pieces.Binary.Length % 20 != 0) { return(false); } torrent._pieces = pieces.Binary; torrent.Version |= BitTorrentVersion.V1; if (info.TryGet("length", out BInteger bLength)) { if (!bLength.TryAs(out long length) || length < 0) { return(false); } torrent.Files = new[] { new FileDescriptor(torrent.DisplayName, length, offset: 0) }; torrent.TotalBytes = length; } else { if (!info.TryGet("files", out BList fileList)) { return(false); } if (fileList.Count < (strictComplianceParsing ? 2 : 1)) { return(false); } var files = new FileDescriptor[fileList.Count]; long offset = 0; for (int i = 0; i < fileList.Count; i++) { if (!(fileList[i] is BDictionary fileEntry)) { return(false); } if (!fileEntry.TryGet("length", out BInteger fileBLength) || !fileBLength.TryAs(out long fileLength)) { return(false); } if (fileLength < 0 || fileLength + offset < 0) { return(false); } if (!fileEntry.TryGet("path", out BList pathList)) { return(false); } if (pathList.Count == 0 || pathList.Count > Constants.MaxFileDirectoryDepth) { return(false); } int totalPathLength = pathList.Count; string[] path = new string[pathList.Count]; for (int j = 0; j < pathList.Count; j++) { if (!(pathList[j] is BString pathBPart) || !pathBPart.IsString) { return(false); } string pathPart = pathBPart.String; if (pathPart.Length == 0) { return(false); } if (!PathHelpers.IsSafeFilePathPart(pathPart)) { return(false); } totalPathLength += pathPart.Length; if ((uint)totalPathLength > Constants.MaxFilePathLength) { return(false); } path[j] = pathPart; } files[i] = new FileDescriptor(path, fileLength, offset); offset += fileLength; } torrent.Files = files; torrent.TotalBytes = offset; } long pieceCount = (torrent.TotalBytes + pieceLength - 1) / pieceLength; // Sanity check on the piece count if (pieceLength < (int)PieceLength.MB_8 && pieceCount > 250000) { return(false); } if (pieceCount * 20 != pieces.Binary.Length) { return(false); } } if (info.TryGet("meta version", out BInteger version)) { if (version.Value == 2) { torrent.Version |= BitTorrentVersion.V2; } else if (torrent.Version != BitTorrentVersion.V1 || !version.Value.IsOne) { return(false); } } else if (torrent.Version == default) { return(false); } if (torrent.Version.HasFlag(BitTorrentVersion.V2)) { if (!info.TryGet("file tree", out BDictionary fileTree)) { return(false); } if (!dictionary.TryGet("piece layers", out BDictionary pieceLayers)) { return(false); } // file tree //throw new NotImplementedException(); } if (info.TryGet("private", out BInteger isPrivate) && isPrivate.Value.IsOne) { torrent.IsPrivate = true; } if (dictionary.TryGet("announce", out BString announce) && announce.IsString) { if (StringHelpers.LooksLikeValidAnnounceURL(announce.String)) { torrent.Trackers.Add(announce.String); } } if (dictionary.TryGet("announce-list", out BList announceList)) { foreach (var announceEntry in announceList) { if (announceEntry is BList announceStringList) { if (announceStringList.Count == 1 && announceStringList[0] is BString announceString) { if (announceString.IsString) { if (StringHelpers.LooksLikeValidAnnounceURL(announceString.String)) { torrent.Trackers.Add(announceString.String); } } } } } } if (dictionary.TryGet("comment", out BString comment) && comment.IsString) { torrent.Comment = comment.String; } if (dictionary.TryGet("created by", out BString createdBy) && createdBy.IsString) { torrent.CreatedBy = createdBy.String; } if (dictionary.TryGet("creation date", out BInteger bcreationDate) && bcreationDate.TryAs(out long creationDate)) { if (creationDate >= 0) { torrent.CreationTimeStamp = creationDate; torrent.CreationDate = DateTime.UnixEpoch.AddSeconds(creationDate); } } if (dictionary.TryGet("encoding", out BString encoding) && encoding.IsString) { torrent.Encoding = encoding.String; } ReadOnlySpan <byte> infoSpan = bytes.Slice(info.SpanStart, info.SpanEnd - info.SpanStart); Debug.Assert(infoSpan[0] == 'd' && infoSpan[infoSpan.Length - 1] == 'e'); if (torrent.Version.HasFlag(BitTorrentVersion.V1)) { torrent.InfoHashV1 = new byte[20]; using SHA1 sha1 = SHA1.Create(); if (!sha1.TryComputeHash(infoSpan, torrent.InfoHashV1, out int written) || written != 20) { return(false); } } if (torrent.Version.HasFlag(BitTorrentVersion.V2)) { torrent.InfoHashV2 = new byte[32]; using SHA256 sha256 = SHA256.Create(); if (!sha256.TryComputeHash(infoSpan, torrent.InfoHashV2, out int written) || written != 32) { return(false); } } return(true); }