/* TessesFramework a library to make C++ easier for me, used in CrossLang: https://git.tesses.org/tesses50/crosslang Copyright (C) 2026 Mike Nolan SPDX-License-Identifier: GPL-3.0-or-later WITH TessesFramework-Exception-1.0 This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "TessesFramework/BitTorrent/TorrentFile.hpp" #include "TessesFramework/Crypto/Crypto.hpp" #include "TessesFramework/Http/HttpUtils.hpp" #include "TessesFramework/Streams/MemoryStream.hpp" namespace Tesses::Framework::BitTorrent { TorrentFileInfo::TorrentFileInfo() { this->fileListOrLength = 0; } TorrentFileInfo::TorrentFileInfo( const Serialization::Bencode::BeDictionary &tkn) { Load(tkn); } int64_t TorrentFileInfo::GetTorrentFileSize() { if (std::holds_alternative(this->fileListOrLength)) return std::get(fileListOrLength); else if (std::holds_alternative>( this->fileListOrLength)) { auto &files = std::get>(this->fileListOrLength); int64_t no = 0; for (auto itm : files) { no += itm.length; } return no; } return 0; } Serialization::Bencode::BeDictionary & TorrentFileInfo::GenerateInfoDictionary() { this->info = {}; this->info.tokens.emplace_back("name", this->name); this->info.tokens.emplace_back("piece length", this->piece_length); this->info.tokens.emplace_back("pieces", this->pieces); if (this->isPrivate) this->info.tokens.emplace_back("private", 1); if (std::holds_alternative(this->fileListOrLength)) { this->info.tokens.emplace_back( "length", std::get(this->fileListOrLength)); } else if (std::holds_alternative>( this->fileListOrLength)) { Serialization::Bencode::BeArray a; for (auto &item : std::get>(this->fileListOrLength)) { Serialization::Bencode::BeDictionary dict; dict.tokens.emplace_back("length", item.length); Serialization::Bencode::BeArray path; for (auto &p : item.path.path) path.tokens.push_back(p); dict.tokens.emplace_back("path", path); a.tokens.push_back(dict); } this->info.tokens.emplace_back("files", a); } return this->info; } Serialization::Bencode::BeDictionary &TorrentFileInfo::GetInfoDictionary() { return this->info; } void TorrentFileInfo::Load(const Serialization::Bencode::BeDictionary &dict) { this->info = dict; auto o = dict.GetValue("name"); if (std::holds_alternative(o)) this->name = std::get(o); else this->name = {}; o = dict.GetValue("piece length"); if (std::holds_alternative(o)) this->piece_length = std::get(o); else this->piece_length = DEFAULT_PIECE_LENGTH; o = dict.GetValue("private"); if (std::holds_alternative(o)) this->isPrivate = std::get(o); else this->isPrivate = 0; o = dict.GetValue("pieces"); if (std::holds_alternative(o)) this->pieces = std::get(o); else this->pieces = {}; o = dict.GetValue("files"); if (std::holds_alternative(o)) { std::vector ents; for (auto &item : std::get(o).tokens) { if (std::holds_alternative( item)) { TorrentFileEntry fe; fe.path.relative = true; auto &d2 = std::get(item); auto o2 = d2.GetValue("length"); if (std::holds_alternative(o2)) { fe.length = std::get(o2); } o2 = d2.GetValue("path"); if (std::holds_alternative( o2)) { auto &arr = std::get(o2); for (auto &itm : arr.tokens) { if (std::holds_alternative< Serialization::Bencode::BeString>(itm)) { fe.path.path.push_back( std::get( itm)); } } } ents.push_back(fe); } } this->fileListOrLength = ents; } else { o = dict.GetValue("length"); if (std::holds_alternative(o)) { this->fileListOrLength = std::get(o); } else { this->fileListOrLength = (int64_t)0; } } } Serialization::Bencode::BeString TorrentFileInfo::GetInfoHash() { auto strm = std::make_shared(true); Serialization::Bencode::Bencode::Save(strm, this->info); strm->Seek(0L, Streams::SeekOrigin::Begin); return Crypto::Sha1::ComputeHash(strm); } TorrentFile::TorrentFile() { this->created_by = "TessesFrameworkTorrent"; this->creation_date = (int64_t)time(NULL); } TorrentFile::TorrentFile(const Serialization::Bencode::BeDictionary &tkn) { auto o = tkn.GetValue("info"); if (std::holds_alternative(o)) { this->info.Load(std::get(o)); } o = tkn.GetValue("announce"); if (std::holds_alternative(o)) { this->announce = std::get(o); } o = tkn.GetValue("announce-list"); if (std::holds_alternative(o)) { auto &ls = std::get(o); for (auto &item : ls.tokens) { if (std::holds_alternative(item)) { auto ls2 = std::get(item); auto item2 = ls2.tokens.at(0); if (std::holds_alternative( item2)) { this->announce_list.push_back( std::get(item2)); } } } } o = tkn.GetValue("creation date"); if (std::holds_alternative(o)) { this->creation_date = std::get(o); } o = tkn.GetValue("comment"); if (std::holds_alternative(o)) { this->comment = std::get(o); } o = tkn.GetValue("created by"); if (std::holds_alternative(o)) { this->created_by = std::get(o); } o = tkn.GetValue("url-list"); if (std::holds_alternative(o)) { auto &li = std::get(o); for (auto &itm : li.tokens) { if (std::holds_alternative(itm)) this->url_list.push_back( std::get(itm)); } } } Serialization::Bencode::BeDictionary TorrentFile::ToDictionary() { Serialization::Bencode::BeDictionary dict; dict.SetValue("info", this->info.GetInfoDictionary()); dict.SetValue("announce", this->announce); if (!this->announce_list.empty()) { Serialization::Bencode::BeArray a; for (auto &item : this->announce_list) { Serialization::Bencode::BeArray a2; a2.tokens.push_back(item); a.tokens.push_back(a2); } dict.SetValue("announce-list", a); } if (!this->url_list.empty()) { Serialization::Bencode::BeArray ls; for (auto &itm : this->url_list) { ls.tokens.push_back(itm); } dict.SetValue("url-list", ls); } dict.SetValue("created by", this->created_by); dict.SetValue("creation date", this->creation_date); dict.SetValue("comment", this->comment); return dict; } void TorrentFile::Print(std::shared_ptr writer) { writer->WriteLine("Announce: " + (std::string)this->announce); writer->WriteLine("Announce List:"); for (auto &item : this->announce_list) { writer->WriteLine("\t" + (std::string)item); } writer->WriteLine("Comment: " + (std::string)this->comment); writer->WriteLine("Created By: " + (std::string)this->created_by); Date::DateTime dt(this->creation_date); dt.SetToLocal(); writer->WriteLine("Creation Date: " + dt.ToString()); writer->WriteLine("Info Hash: " + Http::HttpUtils::BytesToHex( this->info.GetInfoHash().data)); writer->WriteLine("Info:"); writer->WriteLine("\tName: " + (std::string)this->info.name); writer->WriteLine(this->info.isPrivate ? "\tPrivate: true" : "\tPrivate: false"); writer->WriteLine("\tPiece Length: " + TF_FileSize(this->info.piece_length)); if (std::holds_alternative(this->info.fileListOrLength)) { writer->WriteLine("\tIs Single File: true"); writer->WriteLine("\tFile length: " + TF_FileSize((uint64_t)std::get( this->info.fileListOrLength))); } else if (std::holds_alternative>( this->info.fileListOrLength)) { writer->WriteLine("\tIs Single File: false"); writer->WriteLine("\tFiles:"); auto &files = std::get>( this->info.fileListOrLength); for (auto &file : files) { writer->WriteLine("\t\tPath: " + file.path.ToString()); writer->WriteLine("\t\tLength: " + TF_FileSize(file.length)); writer->WriteLine(); } } } std::shared_ptr TorrentFileInfo::GetStreamFromFilesystem( std::shared_ptr vfs, const Tesses::Framework::Filesystem::VFSPath &path) { if (std::holds_alternative>( this->fileListOrLength)) { return std::make_shared( vfs, path / this->name, std::get>(this->fileListOrLength)); } else if (std::holds_alternative(this->fileListOrLength)) { return std::make_shared( vfs, path / this->name, (uint64_t)std::get(this->fileListOrLength)); } return nullptr; } static void ParsePieces(TorrentFileInfo *fi, std::shared_ptr rwa, int64_t flength) { int64_t pieces = fi->pieces.data.size() / 20; std::vector buffer; buffer.resize((size_t)fi->piece_length); size_t lastPieceSize = (size_t)(flength % (int64_t)buffer.size()); for (int64_t i = 0; i < pieces; i++) { size_t len = buffer.size(); if (i == pieces - 1 && lastPieceSize != 0) len = std::min(len, lastPieceSize); rwa->ReadBlockAt(i * buffer.size(), buffer.data(), len); auto hsh = Crypto::Sha1::ComputeHash(buffer.data(), len); std::copy(hsh.begin(), hsh.end(), fi->pieces.data.begin() + (i * 20)); } } void TorrentFileInfo::CreateFromFilesystem( std::shared_ptr vfs, const Tesses::Framework::Filesystem::VFSPath &path, bool isPrivate, int64_t pieceLength) { this->isPrivate = isPrivate; this->piece_length = pieceLength; this->pieces.data.clear(); this->name = path.GetFileName(); int64_t len = 0; if (vfs->FileExists(path)) { auto strm = vfs->OpenFile(path, "rb"); len = strm->GetLength(); this->fileListOrLength = len; int64_t pieces = len / piece_length; if ((len % piece_length) != 0) pieces++; this->pieces.data.resize(pieces * 20); } else if (vfs->DirectoryExists(path)) { std::vector ents; std::function crawl; crawl = [&](Tesses::Framework::Filesystem::VFSPath inFS, Tesses::Framework::Filesystem::VFSPath inTorrent) -> void { for (auto ent : vfs->EnumeratePaths(inFS)) { if (vfs->FileExists(ent)) { auto strm = vfs->OpenFile(ent, "rb"); auto flen = strm->GetLength(); ents.emplace_back(inTorrent / ent.GetFileName(), flen); len += flen; } else if (vfs->DirectoryExists(ent)) { crawl(ent, inTorrent / ent.GetFileName()); } } }; Tesses::Framework::Filesystem::VFSPath p2; p2.relative = true; crawl(path, p2); this->fileListOrLength = ents; int64_t pieces = len / piece_length; if ((len % piece_length) != 0) pieces++; this->pieces.data.resize(pieces * 20); } else { throw std::runtime_error("File or directory does not exist"); } ParsePieces(this, this->GetStreamFromFilesystem(vfs, path.GetParent()), len); this->GenerateInfoDictionary(); } void TorrentFile::CreateTorrent( std::shared_ptr torrent_file_stream, const std::vector &trackers, const std::vector &webseeds, std::shared_ptr vfs, const Tesses::Framework::Filesystem::VFSPath &path, bool isPrivate, int64_t pieceLength, Serialization::Bencode::BeString comment, Serialization::Bencode::BeString created_by) { TorrentFile file; file.announce = trackers.at(0); file.announce_list = trackers; file.url_list = webseeds; file.comment = comment; file.created_by = created_by; file.info.CreateFromFilesystem(vfs, path, isPrivate, pieceLength); auto dict = file.ToDictionary(); Serialization::Bencode::Bencode::Save(torrent_file_stream, dict); } } // namespace Tesses::Framework::BitTorrent