/* Copyright (c) 2005, 2007-2010, 2012-2022, Arvid Norberg Copyright (c) 2016, 2018, 2021, Alden Torres Copyright (c) 2016, Andrei Kurushin Copyright (c) 2016-2018, Steven Siloti Copyright (c) 2016, 2025, Vladimir Golovnev (glassez) Copyright (c) 2018, d-komarov All rights reserved. You may use, distribute and modify this code under the terms of the BSD license, see LICENSE file. */ #include "test.hpp" #include "disk_io_test.hpp" #include "setup_transfer.hpp" #include "test_utils.hpp" #include "settings.hpp" #include "libtorrent/aux_/mmap_storage.hpp" #include "libtorrent/aux_/posix_storage.hpp" #include "libtorrent/aux_/pread_storage.hpp" #include "libtorrent/aux_/file_view_pool.hpp" #include "libtorrent/aux_/file_pool.hpp" #include "libtorrent/hasher.hpp" #include "libtorrent/session.hpp" #include "libtorrent/session_params.hpp" #include "libtorrent/alert_types.hpp" #include "libtorrent/create_torrent.hpp" #include "libtorrent/torrent_info.hpp" #include "libtorrent/read_resume_data.hpp" #include "libtorrent/write_resume_data.hpp" #include "libtorrent/aux_/path.hpp" #include "libtorrent/aux_/storage_utils.hpp" #include "libtorrent/aux_/session_settings.hpp" #include "libtorrent/aux_/stat_cache.hpp" #include "libtorrent/aux_/random.hpp" #include "libtorrent/mmap_disk_io.hpp" #include "libtorrent/posix_disk_io.hpp" #include "libtorrent/pread_disk_io.hpp" #include "libtorrent/flags.hpp" #include "libtorrent/aux_/readwrite.hpp" #include "libtorrent/load_torrent.hpp" #include #include // for bind #include #include using namespace std::placeholders; using namespace lt; namespace { #if TORRENT_HAVE_MMAP || TORRENT_HAVE_MAP_VIEW_OF_FILE using lt::aux::mmap_storage; #endif using lt::aux::posix_storage; using lt::aux::pread_storage; constexpr int piece_size = 16 * 1024 * 16; constexpr int half = piece_size / 2; void delete_dirs(std::string path) { path = complete(path); error_code ec; remove_all(path, ec); if (ec && ec != boost::system::errc::no_such_file_or_directory) { std::printf("remove_all \"%s\": %s\n" , path.c_str(), ec.message().c_str()); } TEST_CHECK(!exists(path)); } void on_check_resume_data(lt::status_t const status, storage_error const& error, bool* done, bool* oversized) { std::cout << time_now_string() << " on_check_resume_data ret: " << int(static_cast(status)); if (status & lt::disk_status::oversized_file) { std::cout << " oversized file(s) - "; *oversized = true; } else { *oversized = false; } if (status & lt::disk_status::fatal_disk_error) { std::cout << " disk error: " << error.ec.message() << " file: " << error.file() << std::endl; } else if (status & lt::disk_status::need_full_check) { std::cout << " need full check" << std::endl; } else if (status & lt::disk_status::file_exist) { std::cout << " file exist" << std::endl; } else { std::cout << " success" << std::endl; } std::cout << std::endl; *done = true; } void on_piece_checked(piece_index_t, sha1_hash const& , storage_error const& error, bool* done) { std::cout << time_now_string() << " on_piece_checked err: " << error.ec.message() << '\n'; *done = true; } void print_error(char const* call, int ret, storage_error const& ec) { std::printf("%s: %s() returned: %d error: \"%s\" in file: %d operation: %s\n" , time_now_string().c_str(), call, ret, ec.ec.message().c_str() , static_cast(ec.file()), operation_name(ec.operation)); } void run_until(io_context& ios, bool const& done) { while (!done) { ios.restart(); ios.run_one_for(lt::milliseconds(100)); std::cout << time_now_string() << " done: " << done << std::endl; } } lt::aux::torrent_info_ptr setup_torrent_info(std::vector& buf) { std::vector fs; fs.emplace_back(combine_path("temp_storage", "test1.tmp"), 0x8000); fs.emplace_back(combine_path("temp_storage", combine_path("folder1", "test2.tmp")), 0x8000); fs.emplace_back(combine_path("temp_storage", combine_path("folder2", "test3.tmp")), 0); fs.emplace_back(combine_path("temp_storage", combine_path("_folder3", "test4.tmp")), 0); fs.emplace_back(combine_path("temp_storage", combine_path("_folder3", combine_path("subfolder", "test5.tmp"))), 0x8000); lt::create_torrent t(std::move(fs), 0x4000); sha1_hash h = hasher(std::vector(0x4000, 0)).final(); for (piece_index_t i(0); i < 6_piece; ++i) t.set_hash(i, h); buf = bencode(t.generate()); return load_torrent_buffer(buf).ti; } // file_pool_type is a meta function returning the file pool type for a specific // StorageType // maybe this would be easier to follow if it was all bundled up in a // traits class template struct file_pool_type {}; #if TORRENT_HAVE_MMAP || TORRENT_HAVE_MAP_VIEW_OF_FILE template <> struct file_pool_type { using type = aux::file_view_pool; }; #endif template <> struct file_pool_type { using type = int; }; template <> struct file_pool_type { using type = aux::file_pool; }; template std::shared_ptr make_storage(storage_params const& p , typename file_pool_type::type& fp); #if TORRENT_HAVE_MMAP || TORRENT_HAVE_MAP_VIEW_OF_FILE template <> std::shared_ptr make_storage(storage_params const& p , aux::file_view_pool& fp) { return std::make_shared(p, fp); } #endif template <> std::shared_ptr make_storage(storage_params const& p , int&) { return std::make_shared(p); } template <> std::shared_ptr make_storage(storage_params const& p , aux::file_pool& fp) { return std::make_shared(p, fp); } template std::pair, std::shared_ptr> setup_torrent( FilePool& fp , std::vector& buf , std::string const& test_path , aux::session_settings& set) { auto info = setup_torrent_info(buf); aux::vector priorities; renamed_files rf; storage_params p{ info->layout(), rf, test_path, {}, storage_mode_allocate, priorities, sha1_hash{}, info->v1(), info->v2() }; auto s = make_storage(p, fp); // allocate the files and create the directories storage_error se; s->initialize(set, se); if (se) { TEST_ERROR(se.ec.message().c_str()); std::printf("mmap_storage::initialize %s: %d\n" , se.ec.message().c_str(), static_cast(se.file())); throw system_error(se.ec); } return {s, info}; } #if TORRENT_HAVE_MMAP || TORRENT_HAVE_MAP_VIEW_OF_FILE int write(std::shared_ptr s , aux::session_settings const& sett , span buf , piece_index_t const piece, int const offset , aux::open_mode_t const mode , storage_error& error) { return s->write(sett, buf, piece, offset, mode, disk_job_flags_t{}, error); } int read(std::shared_ptr s , aux::session_settings const& sett , span buf , piece_index_t piece , int const offset , aux::open_mode_t mode , storage_error& ec) { return s->read(sett, buf, piece, offset, mode, disk_job_flags_t{}, ec); } void release_files(std::shared_ptr s, storage_error& ec) { s->release_files(ec); } #endif int write(std::shared_ptr s , aux::session_settings const& sett , span buf , piece_index_t const piece , int const offset , aux::open_mode_t , storage_error& error) { return s->write(sett, buf, piece, offset, error); } int read(std::shared_ptr s , aux::session_settings const& sett , span buf , piece_index_t piece , int offset , aux::open_mode_t , storage_error& ec) { return s->read(sett, buf, piece, offset, ec); } void release_files(std::shared_ptr, storage_error&) {} int write(std::shared_ptr s , aux::session_settings const& sett , span buf , piece_index_t const piece , int const offset , aux::open_mode_t mode , storage_error& error) { return s->write(sett, buf, piece, offset, mode, disk_job_flags_t{}, error); } int read(std::shared_ptr s , aux::session_settings const& sett , span buf , piece_index_t piece , int offset , aux::open_mode_t mode , storage_error& ec) { return s->read(sett, buf, piece, offset, mode, disk_job_flags_t{}, ec); } void release_files(std::shared_ptr s, storage_error& ec) { s->release_files(ec); } std::vector new_piece(std::size_t const size) { std::vector ret(size); aux::random_bytes(ret); return ret; } template void run_storage_tests(std::shared_ptr info , lt::storage_mode_t storage_mode) { lt::file_storage const& fs = info->layout(); TORRENT_ASSERT(fs.num_files() > 0); { error_code ec; create_directory(complete("temp_storage"), ec); if (ec) std::cout << "create_directory '" << complete("temp_storage") << "': " << ec.message() << std::endl; } int const num_pieces = fs.num_pieces(); TEST_EQUAL(info->num_pieces(), num_pieces); std::vector piece0 = new_piece(piece_size); std::vector piece1 = new_piece(piece_size); std::vector piece2 = new_piece(piece_size); aux::session_settings set; std::vector piece(piece_size); { // avoid having two storages use the same files typename file_pool_type::type fp; boost::asio::io_context ios; aux::vector priorities; std::string const cwd = current_working_directory(); renamed_files rf; storage_params p{ fs, rf, cwd, {}, storage_mode, priorities, sha1_hash{}, info->v1(), info->v2(), }; auto s = make_storage(p, fp); storage_error ec; s->initialize(set, ec); TEST_CHECK(!ec); if (ec) print_error("initialize", 0, ec); int ret = 0; // write piece 1 (in slot 0) span iov = span(piece1).first(half); ret = write(s, set, iov, 0_piece, 0, aux::open_mode::write, ec); TEST_EQUAL(ret, int(iov.size())); if (ret != int(iov.size())) print_error("write", ret, ec); iov = span(piece1).last(half); ret = write(s, set, iov, 0_piece, half, aux::open_mode::write, ec); TEST_EQUAL(ret, int(iov.size())); if (ret != int(iov.size())) print_error("write", ret, ec); // test unaligned read (where the bytes are aligned) iov = span(piece).subspan(3, piece_size - 9); ret = read(s, set, iov, 0_piece, 3, aux::open_mode::write, ec); TEST_EQUAL(ret, int(iov.size())); if (ret != int(iov.size())) print_error("read",ret, ec); TEST_CHECK(iov == span(piece1).subspan(3, piece_size - 9)); // test unaligned read (where the bytes are not aligned) iov = span(piece).first(piece_size - 9); ret = read(s, set, iov, 0_piece, 3, aux::open_mode::write, ec); TEST_EQUAL(ret, int(iov.size())); if (ret != int(iov.size())) print_error("read", ret, ec); TEST_CHECK(iov == span(piece1).subspan(3, piece_size - 9)); // verify piece 1 iov = piece; ret = read(s, set, iov, 0_piece, 0, aux::open_mode::write, ec); TEST_EQUAL(ret, int(iov.size())); if (ret != int(iov.size())) print_error("read", ret, ec); TEST_CHECK(piece == piece1); // do the same with piece 0 and 2 (in slot 1 and 2) iov = piece0; ret = write(s, set, iov, 1_piece, 0, aux::open_mode::write, ec); TEST_EQUAL(ret, int(iov.size())); if (ret != int(iov.size())) print_error("write", ret, ec); iov = piece2; ret = write(s, set, iov, 2_piece, 0, aux::open_mode::write, ec); TEST_EQUAL(ret, int(iov.size())); if (ret != int(iov.size())) print_error("write", ret, ec); // verify piece 0 and 2 iov = piece; ret = read(s, set, iov, 1_piece, 0, aux::open_mode::write, ec); TEST_EQUAL(ret, int(iov.size())); if (ret != int(iov.size())) print_error("read", ret, ec); TEST_CHECK(piece == piece0); iov = piece; ret = read(s, set, iov, 2_piece, 0, aux::open_mode::write, ec); TEST_EQUAL(ret, int(iov.size())); if (ret != int(piece_size)) print_error("read", ret, ec); TEST_CHECK(piece == piece2); release_files(s, ec); } } template void test_remove(std::string const& test_path) { delete_dirs("temp_storage"); std::vector buf; typename file_pool_type::type fp; io_context ios; aux::session_settings set; auto [s, info] = setup_torrent(fp, buf, test_path, set); // directories are not created up-front, unless they contain // an empty file (all of which are created up-front, along with // all required directories) // files are created on first write TEST_CHECK(!exists(combine_path(test_path, combine_path("temp_storage" , combine_path("_folder3", combine_path("subfolder", "test5.tmp")))))); // this directory and file is created up-front because it's an empty file TEST_CHECK(exists(combine_path(test_path, combine_path("temp_storage" , combine_path("folder2", "test3.tmp"))))); // this isn't TEST_CHECK(!exists(combine_path(test_path, combine_path("temp_storage" , combine_path("folder1", "test2.tmp"))))); buf.resize(0x4000); span b = {&buf[0], 0x4000}; storage_error se; write(s, set, b, 2_piece, 0, aux::open_mode::write, se); TEST_CHECK(exists(combine_path(test_path, combine_path("temp_storage" , combine_path("folder1", "test2.tmp"))))); TEST_CHECK(!exists(combine_path(test_path, combine_path("temp_storage" , combine_path("_folder3", combine_path("subfolder", "test5.tmp")))))); file_status st; error_code ec; stat_file(combine_path(test_path, combine_path("temp_storage" , combine_path("folder1", "test2.tmp"))), &st, ec); // if the storage truncates the file to the full size, it's 8, otherwise it's // 4 TEST_CHECK(st.file_size == 0x8000 || st.file_size == 0x4000); write(s, set, b, 0_piece, 0, aux::open_mode::write, se); TEST_CHECK(exists(combine_path(test_path, combine_path("temp_storage" , combine_path("_folder3", combine_path("subfolder", "test5.tmp")))))); stat_file(combine_path(test_path, combine_path("temp_storage" , combine_path("_folder3", "test5.tmp"))), &st, ec); // if the storage truncates the file to the full size, it's 8, otherwise it's // 4 TEST_CHECK(st.file_size == 0x8000 || st.file_size == 0x4000); s->delete_files(session::delete_files, se); if (se) print_error("delete_files", 0, se); if (se) { TEST_ERROR(se.ec.message().c_str()); std::printf("mmap_storage::delete_files %s: %d\n" , se.ec.message().c_str(), static_cast(se.file())); } TEST_CHECK(!exists(combine_path(test_path, "temp_storage"))); } template void test_rename(std::string const& test_path) { delete_dirs("temp_storage"); std::vector buf; typename file_pool_type::type fp; io_context ios; aux::session_settings set; auto [s, info] = setup_torrent(fp, buf, test_path, set); file_storage const& fs = info->layout(); // directories are not created up-front, unless they contain // an empty file std::string first_file = fs.file_path(0_file); for (auto const i : fs.file_range()) { TEST_CHECK(!exists(combine_path(test_path, combine_path("temp_storage" , fs.file_path(i))))); } storage_error se; s->rename_file(0_file, "new_filename", se); if (se.ec) { std::printf("mmap_storage::rename_file failed: %s\n" , se.ec.message().c_str()); } TEST_CHECK(!se.ec); TEST_EQUAL(s->names().file_path(0_file), "new_filename"); } #if TORRENT_HAVE_MMAP || TORRENT_HAVE_MAP_VIEW_OF_FILE namespace { std::int64_t file_size_on_disk(std::string const& path) { #ifdef TORRENT_WINDOWS native_path_string f = convert_to_native_path_string(path); // in order to open a directory, we need the FILE_FLAG_BACKUP_SEMANTICS HANDLE h = CreateFileW(f.c_str(), 0, FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); TEST_CHECK(h != INVALID_HANDLE_VALUE); FILE_STANDARD_INFO Standard; TEST_CHECK(GetFileInformationByHandleEx(h, FILE_INFO_BY_HANDLE_CLASS::FileStandardInfo, &Standard, sizeof(FILE_STANDARD_INFO))); CloseHandle(h); return Standard.AllocationSize.QuadPart; #else struct ::stat st{}; TEST_EQUAL(::stat(path.c_str(), &st), 0); return std::int64_t(st.st_blocks) * 512; #endif } } template void test_pre_allocate() { std::string const test_path = complete("pre_allocate_test_path"); delete_dirs(combine_path(test_path, "temp_storage")); bool const supports_prealloc = fs_supports_prealloc(); std::vector buf; typename file_pool_type::type fp; io_context ios; aux::session_settings set; std::shared_ptr info = setup_torrent_info(buf); file_storage const& fs = info->layout(); aux::vector priorities{ lt::dont_download, lt::default_priority, lt::default_priority, lt::default_priority, lt::default_priority, }; renamed_files rf; storage_params p{ info->layout(), rf, test_path, {}, storage_mode_allocate, priorities, sha1_hash{}, info->v1(), info->v2() }; auto s = make_storage(p, fp); // allocate the files and create the directories storage_error se; s->initialize(set, se); if (se) { TEST_ERROR(se.ec.message().c_str()); std::printf("storage::initialize %s: %d\n" , se.ec.message().c_str(), static_cast(se.file())); throw system_error(se.ec); } std::vector piece1 = new_piece(0x4000); span iov = span(piece1); // ensure all files, except the first one, have been allocated for (auto i : fs.file_range()) { if (fs.file_size(i) > 0) { int ret = write(s, set, iov, fs.piece_index_at_file(i), 0, aux::open_mode::write, se); TEST_EQUAL(ret, int(iov.size())); TEST_CHECK(!se.ec); } error_code ec; file_status st; std::string const path = fs.file_path(i, test_path); stat_file(path, &st, ec); if (i == file_index_t{0}) { // the first file has priority 0, and so should not be created TEST_EQUAL(ec, boost::system::errc::no_such_file_or_directory); } else { TEST_CHECK(!ec); std::cerr << "error: " << ec.message() << std::endl; TEST_EQUAL(st.file_size, fs.file_size(i)); if (supports_prealloc || fs.file_size(i) == 0) { TEST_CHECK(file_size_on_disk(path) >= fs.file_size(i)); } else { TEST_CHECK(file_size_on_disk(path) <= fs.file_size(i)); } } } std::cerr << "set file priority" << std::endl; // set priority of file 0 to non-zero, and make sure we create the file now priorities[0_file] = lt::default_priority; s->set_file_priority(set, priorities, se); TEST_CHECK(!se.ec); for (auto i : fs.file_range()) { error_code ec; file_status st; std::string const path = fs.file_path(i, test_path); stat_file(path, &st, ec); std::cerr << "error: " << ec.message() << std::endl; TEST_CHECK(!ec); if (supports_prealloc || fs.file_size(i) == 0) { TEST_CHECK(file_size_on_disk(path) >= fs.file_size(i)); } else { TEST_CHECK(file_size_on_disk(path) <= fs.file_size(i)); } } } #endif // TORRENT_HAVE_MMAP || TORRENT_HAVE_MAP_VIEW_OF_FILE using lt::operator""_bit; using check_files_flag_t = lt::flags::bitfield_flag; constexpr check_files_flag_t sparse = 0_bit; constexpr check_files_flag_t test_oversized = 1_bit; constexpr check_files_flag_t zero_prio = 2_bit; void test_check_files(check_files_flag_t const flags, lt::disk_io_constructor_type const disk_constructor, int const aio_threads = 1, int const hashing_threads = 0) { std::string const test_path = current_working_directory(); error_code ec; constexpr int piece_size_check = 16 * 1024; delete_dirs("temp_storage"); std::vector fs; fs.emplace_back("temp_storage/test1.tmp", piece_size_check); fs.emplace_back("temp_storage/test2.tmp", piece_size_check * 2); fs.emplace_back("temp_storage/test3.tmp", piece_size_check); std::vector piece0 = new_piece(piece_size_check); std::vector piece2 = new_piece(piece_size_check); lt::create_torrent t(std::move(fs), piece_size_check); t.set_hash(0_piece, hasher(piece0).final()); t.set_hash(1_piece, sha1_hash::max()); t.set_hash(2_piece, sha1_hash::max()); t.set_hash(3_piece, hasher(piece2).final()); create_directory(combine_path(test_path, "temp_storage"), ec); if (ec) std::cout << "create_directory: " << ec.message() << std::endl; if (flags & test_oversized) piece2.push_back(0x42); ofstream(combine_path(test_path, combine_path("temp_storage", "test1.tmp")).c_str()) .write(piece0.data(), std::streamsize(piece0.size())); ofstream(combine_path(test_path, combine_path("temp_storage", "test3.tmp")).c_str()) .write(piece2.data(), std::streamsize(piece2.size())); std::shared_ptr info = load_torrent_buffer(bencode(t.generate())).ti; aux::session_settings set; boost::asio::io_context ios; counters cnt; aux::session_settings sett; sett.set_int(settings_pack::aio_threads, aio_threads); sett.set_int(settings_pack::hashing_threads, hashing_threads); std::unique_ptr io = disk_constructor(ios, sett, cnt); aux::vector priorities; if (flags & zero_prio) priorities.resize(std::size_t(info->num_files()), download_priority_t{}); renamed_files rf; storage_params p{ info->layout(), rf, test_path, {}, (flags & sparse) ? storage_mode_sparse : storage_mode_allocate, priorities, sha1_hash{}, info->v1(), info->v2() }; auto st = io->new_torrent(std::move(p), std::shared_ptr()); bool done = false; bool oversized = false; add_torrent_params frd; aux::vector links; io->async_check_files(st, &frd, links , std::bind(&on_check_resume_data, _1, _2, &done, &oversized)); io->submit_jobs(); ios.restart(); run_until(ios, done); TEST_EQUAL(oversized, bool(flags & test_oversized)); for (auto const i : info->piece_range()) { done = false; io->async_hash(st, i, {} , disk_interface::sequential_access | disk_interface::volatile_read | disk_interface::v1_hash , std::bind(&on_piece_checked, _1, _2, _3, &done)); io->submit_jobs(); ios.restart(); run_until(ios, done); } int outstanding = 3; int hashes_done = 0; bool release_done = false; sha1_hash const expected_hash = hasher(piece0).final(); auto hash_handler = [&](piece_index_t const piece, sha1_hash const& hash, storage_error const& error) { TEST_EQUAL(piece, 0_piece); TEST_CHECK(!error); TEST_EQUAL(hash, expected_hash); ++hashes_done; --outstanding; }; io->async_hash(st, 0_piece, {}, disk_interface::sequential_access | disk_interface::volatile_read | disk_interface::v1_hash, hash_handler); io->async_release_files(st, [&] { release_done = true; --outstanding; }); io->async_hash(st, 0_piece, {}, disk_interface::sequential_access | disk_interface::volatile_read | disk_interface::v1_hash, hash_handler); io->submit_jobs(); time_point const end_time = clock_type::now() + seconds(10); while (outstanding > 0 && clock_type::now() < end_time) { ios.run_one_for(milliseconds(100)); ios.restart(); } TEST_EQUAL(outstanding, 0); TEST_EQUAL(hashes_done, 2); TEST_CHECK(release_done); io->abort(true); } // TODO: 2 split this test up into smaller parts template void run_test() { std::string const test_path = current_working_directory(); std::cout << "\n=== " << test_path << " ===\n" << std::endl; std::vector piece0 = new_piece(piece_size); std::vector piece1 = new_piece(piece_size); std::vector piece2 = new_piece(piece_size); std::vector piece3 = new_piece(piece_size); delete_dirs("temp_storage"); std::vector fs; fs.emplace_back("temp_storage/test1.tmp", 17); fs.emplace_back("temp_storage/test2.tmp", 612); fs.emplace_back("temp_storage/test3.tmp", 0); fs.emplace_back("temp_storage/test4.tmp", 0); fs.emplace_back("temp_storage/test5.tmp", 3253); fs.emplace_back("temp_storage/test6.tmp", 841); int const last_file_size = 4 * int(piece_size) - (17 + 612 + 3253 + 841); fs.emplace_back("temp_storage/test7.tmp", last_file_size); // File layout // +-+--+++-------+-------+----------------------------------------------------------------------------------------+ // |1| 2||| file5 | file6 | file7 | // +-+--+++-------+-------+----------------------------------------------------------------------------------------+ // | | | | | // | piece 0 | piece 1 | piece 2 | piece 3 | lt::create_torrent t(std::move(fs), piece_size, create_torrent::v1_only); TEST_CHECK(t.num_pieces() == 4); t.set_hash(0_piece, hasher(piece0).final()); t.set_hash(1_piece, hasher(piece1).final()); t.set_hash(2_piece, hasher(piece2).final()); t.set_hash(3_piece, hasher(piece3).final()); std::shared_ptr info = load_torrent_buffer(bencode(t.generate())).ti; for (auto storage_mode : { storage_mode_sparse, storage_mode_allocate }) { // run_storage_tests writes piece 0, 1 and 2. not 3 run_storage_tests(info, storage_mode); // make sure the files have the correct size std::string const base = complete("temp_storage"); // these files should have been allocated as 0 size TEST_CHECK(exists(combine_path(base, "test3.tmp"))); TEST_CHECK(exists(combine_path(base, "test4.tmp"))); delete_dirs("temp_storage"); } } namespace { void test_check_files_all_threads( check_files_flag_t const flags, lt::disk_io_constructor_type disk_io) { test_check_files(flags, disk_io); // posix_disk_io is single-threaded; the thread-count variants are redundant if (is_single_threaded_disk_io(disk_io)) return; test_check_files(flags, disk_io, 0, 1); test_check_files(flags, disk_io, 0, 2); } } // anonymous namespace TORRENT_TEST_DISK_IO(check_files_sparse) { test_check_files_all_threads(sparse | zero_prio, disk_io); } TORRENT_TEST_DISK_IO(check_files_oversized_zero_prio) { test_check_files_all_threads(sparse | zero_prio | test_oversized, disk_io); } TORRENT_TEST_DISK_IO(check_files_oversized) { test_check_files_all_threads(sparse | test_oversized, disk_io); } TORRENT_TEST_DISK_IO(check_files_allocate) { test_check_files_all_threads(zero_prio, disk_io); } #if TORRENT_HAVE_MMAP || TORRENT_HAVE_MAP_VIEW_OF_FILE TORRENT_TEST(test_pre_allocate_mmap) { test_pre_allocate(); } #endif // posix_storage is meant to only use the most portable API for disk I/O, and so // doesn't support pre-allocating files /* TORRENT_TEST(test_pre_allocate_posix) { test_pre_allocate(); } */ #if TORRENT_HAVE_MMAP || TORRENT_HAVE_MAP_VIEW_OF_FILE TORRENT_TEST(rename_mmap_disk_io) { test_rename(current_working_directory()); } TORRENT_TEST(remove_mmap_disk_io) { test_remove(current_working_directory()); } #endif TORRENT_TEST(rename_posix_disk_io) { test_rename(current_working_directory()); } TORRENT_TEST(remove_posix_disk_io) { test_remove(current_working_directory()); } TORRENT_TEST(rename_pread_disk_io) { test_rename(current_working_directory()); } TORRENT_TEST(remove_pread_disk_io) { test_remove(current_working_directory()); } void test_fastresume(bool const test_deprecated) { std::string test_path = current_working_directory(); error_code ec; std::cout << "\n\n=== test fastresume ===" << std::endl; delete_dirs("tmp1"); create_directory(combine_path(test_path, "tmp1"), ec); if (ec) std::cout << "create_directory '" << combine_path(test_path, "tmp1") << "': " << ec.message() << std::endl; ofstream file(combine_path(test_path, "tmp1/temporary").c_str()); add_torrent_params atp = ::create_torrent(&file); file.close(); TEST_CHECK(exists(complete("tmp1/temporary"))); if (!exists(complete("tmp1/temporary"))) return; entry resume; { settings_pack pack = settings(); lt::session ses(pack); add_torrent_params p = atp; p.save_path = combine_path(test_path, "tmp1"); p.storage_mode = storage_mode_sparse; error_code ignore; torrent_handle h = ses.add_torrent(std::move(p), ignore); torrent_status s = h.status(); TEST_CHECK(exists(combine_path(s.save_path, "temporary"))); if (!exists(combine_path(s.save_path, "temporary"))) return; for (int i = 0; i < 50; ++i) { print_alerts(ses, "ses"); s = h.status(); std::cout << "progress: " << s.progress << std::endl; if (s.progress == 1.0f) { std::cout << "progress: 1.0f" << std::endl; break; } std::this_thread::sleep_for(lt::milliseconds(100)); } // the whole point of the test is to have a resume // data which expects the file to exist in full. If // we failed to do that, we might as well abort TEST_EQUAL(int(s.progress * 1000), 1000); if (s.progress != 1.0f) return; h.save_resume_data(); alert const* ra = wait_for_alert(ses, save_resume_data_alert::alert_type); TEST_CHECK(ra); if (ra) resume = write_resume_data(alert_cast(ra)->params); ses.remove_torrent(h, lt::session::delete_files); alert const* da = wait_for_alert(ses, torrent_deleted_alert::alert_type); TEST_CHECK(da); } TEST_CHECK(!exists(combine_path(test_path, combine_path("tmp1", "temporary")))); if (exists(combine_path(test_path, combine_path("tmp1", "temporary")))) return; std::cout << resume.to_string() << "\n"; // make sure the fast resume check fails! since we removed the file { settings_pack pack = settings(); lt::session ses(pack); std::vector const resume_data = bencode(resume); add_torrent_params p; TORRENT_UNUSED(test_deprecated); #if TORRENT_ABI_VERSION == 1 if (test_deprecated) { p.resume_data = resume_data; } else #endif { p = read_resume_data(resume_data); } p.flags &= ~torrent_flags::paused; p.flags &= ~torrent_flags::auto_managed; p.ti = atp.ti; p.save_path = combine_path(test_path, "tmp1"); p.storage_mode = storage_mode_sparse; torrent_handle h = ses.add_torrent(std::move(p), ec); std::printf("expecting fastresume to be rejected because the files were removed"); alert const* a = wait_for_alert(ses, fastresume_rejected_alert::alert_type , "ses"); // we expect the fast resume to be rejected because the files were removed TEST_CHECK(alert_cast(a) != nullptr); } delete_dirs("tmp1"); } } // anonymous namespace TORRENT_TEST(fastresume) { test_fastresume(false); } #if TORRENT_ABI_VERSION == 1 TORRENT_TEST(fastresume_deprecated) { test_fastresume(true); } #endif TORRENT_TEST(fastresume_spanning_piece_missing_file) { int const resume_piece_size = 0x4000; std::string const save_path = complete("temp_storage_spanning_resume"); delete_dirs(save_path); file_storage fs; fs.add_file(combine_path("test", "first"), resume_piece_size / 2); fs.add_file(combine_path("test", "missing"), resume_piece_size / 2); fs.add_file(combine_path("test", "unchecked"), resume_piece_size); fs.set_piece_length(resume_piece_size); fs.set_num_pieces(aux::calc_num_pieces(fs)); error_code ec; create_directories(combine_path(save_path, "test"), ec); TEST_CHECK(!ec); std::ofstream file(fs.file_path(0_file, save_path).c_str()); file.write("x", 1); file.close(); add_torrent_params rd; rd.have_pieces.resize(fs.num_pieces(), false); rd.have_pieces.set_bit(0_piece); aux::vector links; aux::vector priorities; aux::stat_cache stat; storage_error se; lt::renamed_files const rf; lt::filenames const fn(fs, rf); bool const valid = aux::verify_resume_data(rd, links, fn, priorities, stat, save_path, se); TEST_CHECK(!valid); TEST_EQUAL(se.ec, error_code(errors::mismatching_file_size)); TEST_EQUAL(static_cast(se.file()), 1); delete_dirs(save_path); } namespace { bool got_file_rename_alert(alert const* a) { return alert_cast(a) || alert_cast(a); } } // anonymous namespace TORRENT_TEST(rename_file) { std::vector buf; auto info = setup_torrent_info(buf); file_storage const& fs = info->layout(); settings_pack pack = settings(); pack.set_bool(settings_pack::disable_hash_checks, true); lt::session ses(pack); add_torrent_params p; p.ti = info; p.save_path = "."; error_code ec; torrent_handle h = ses.add_torrent(std::move(p), ec); // prevent race conditions of adding pieces while checking lt::torrent_status st = h.status(); for (int i = 0; i < 40; ++i) { print_alerts(ses, "ses", true, true); st = h.status(); if (st.state != torrent_status::checking_files && st.state != torrent_status::checking_resume_data) break; std::this_thread::sleep_for(lt::milliseconds(100)); } // make it a seed std::vector tmp(std::size_t(info->piece_length())); for (auto const i : fs.piece_range()) h.add_piece(i, &tmp[0]); // wait for the files to have been written for (int i = 0; i < info->num_pieces(); ++i) { alert const* pf = wait_for_alert(ses, piece_finished_alert::alert_type , "ses", pop_alerts::cache_alerts); TEST_CHECK(pf); } // now rename them. This is the test for (auto const i : fs.file_range()) { std::string name = fs.file_path(i); h.rename_file(i, "temp_storage__" + name.substr(12)); } // wait for the files to have been renamed for (int i = 0; i < info->num_files(); ++i) { alert const* fra = wait_for_alert(ses, file_renamed_alert::alert_type , "ses", pop_alerts::cache_alerts); TEST_CHECK(fra); } TEST_CHECK(exists(info->name() + "__")); // verify get_renamed_files() reflects all renames synchronously lt::renamed_files const rf = h.get_renamed_files(); lt::filenames fn(fs, rf); for (auto const i : fs.file_range()) { std::string name = fs.file_path(i); TEST_EQUAL(fn.file_path(i), "temp_storage__" + name.substr(12)); } h.save_resume_data(); alert const* ra = wait_for_alert(ses, save_resume_data_alert::alert_type); TEST_CHECK(ra); if (!ra) return; add_torrent_params resume = alert_cast(ra)->params; auto const files = resume.renamed_files; for (auto const& i : files) { TEST_EQUAL(i.second.substr(0, 14), "temp_storage__"); } } namespace { void test_rename_file_fastresume(bool test_deprecated) { std::string test_path = current_working_directory(); error_code ec; std::cout << "\n\n=== test rename file in fastresume ===" << std::endl; delete_dirs("tmp2"); create_directory(combine_path(test_path, "tmp2"), ec); if (ec) std::cout << "create_directory: " << ec.message() << std::endl; ofstream file(combine_path(test_path, "tmp2/temporary").c_str()); add_torrent_params atp = ::create_torrent(&file); file.close(); TEST_CHECK(exists(combine_path(test_path, "tmp2/temporary"))); add_torrent_params resume; { settings_pack pack = settings(); lt::session ses(pack); add_torrent_params p; p.ti = atp.ti; p.save_path = combine_path(test_path, "tmp2"); p.storage_mode = storage_mode_sparse; torrent_handle h = ses.add_torrent(std::move(p), ec); h.rename_file(0_file, "testing_renamed_files"); std::cout << "renaming file" << std::endl; bool renamed = false; for (int i = 0; i < 30; ++i) { if (print_alerts(ses, "ses", true, true, &got_file_rename_alert)) renamed = true; torrent_status s = h.status(); if (s.state == torrent_status::seeding && renamed) break; std::this_thread::sleep_for(lt::milliseconds(100)); } std::cout << "stop loop" << std::endl; torrent_status s = h.status(); TEST_CHECK(s.state == torrent_status::seeding); h.save_resume_data(); alert const* ra = wait_for_alert(ses, save_resume_data_alert::alert_type); TEST_CHECK(ra); if (ra) resume = alert_cast(ra)->params; ses.remove_torrent(h); } TEST_CHECK(!exists(combine_path(test_path, "tmp2/temporary"))); TEST_CHECK(exists(combine_path(test_path, "tmp2/testing_renamed_files"))); TEST_CHECK(!resume.renamed_files.empty()); entry resume_ent = write_resume_data(resume); std::cout << resume_ent.to_string() << "\n"; // make sure the fast resume check succeeds, even though we renamed the file { settings_pack pack = settings(); lt::session ses(pack); add_torrent_params p; std::vector const resume_data = bencode(resume_ent); TORRENT_UNUSED(test_deprecated); #if TORRENT_ABI_VERSION == 1 if (test_deprecated) { p.resume_data = resume_data; } else #endif { p = read_resume_data(resume_data); } p.ti = atp.ti; p.save_path = combine_path(test_path, "tmp2"); p.storage_mode = storage_mode_sparse; torrent_handle h = ses.add_torrent(std::move(p), ec); torrent_status stat; for (int i = 0; i < 50; ++i) { stat = h.status(); print_alerts(ses, "ses"); if (stat.state == torrent_status::seeding) break; std::this_thread::sleep_for(lt::milliseconds(100)); } TEST_CHECK(stat.state == torrent_status::seeding); h.save_resume_data(); alert const* ra = wait_for_alert(ses, save_resume_data_alert::alert_type); TEST_CHECK(ra); if (ra) resume = alert_cast(ra)->params; ses.remove_torrent(h); } TEST_CHECK(!resume.renamed_files.empty()); resume_ent = write_resume_data(resume); std::cout << resume_ent.to_string() << "\n"; remove_all(combine_path(test_path, "tmp2"), ec); if (ec && ec != boost::system::errc::no_such_file_or_directory) std::cout << "remove_all '" << combine_path(test_path, "tmp2") << "': " << ec.message() << std::endl; } } // anonymous namespace TORRENT_TEST(rename_file_fastresume) { test_rename_file_fastresume(false); } #if TORRENT_ABI_VERSION == 1 TORRENT_TEST(rename_file_fastresume_deprecated) { test_rename_file_fastresume(true); } #endif namespace { void fill_pattern(span buf) { int counter = 0; for (char& v : buf) { v = char(counter & 0xff); ++counter; } } void fill_pattern(span const> bufs) { int counter = 0; for (auto const& buf : bufs) { for (char& v : buf) { v = char(counter & 0xff); ++counter; } } } bool check_pattern(std::vector const& buf, int counter) { unsigned char const* p = reinterpret_cast(buf.data()); for (int k = 0; k < int(buf.size()); ++k) { if (p[k] != (counter & 0xff)) return false; ++counter; } return true; } template void alloc_iov(span* iov, int num_bufs) { for (std::size_t i = 0; i < static_cast(num_bufs); ++i) { std::size_t const len = static_cast(num_bufs) * (i + 1); iov[i] = { new char[len], static_cast(len) }; } } // TODO: this should take a span template void free_iov(span* iov, int num_bufs) { for (int i = 0; i < num_bufs; ++i) { delete[] iov[i].data(); iov[i] = { nullptr, 0 }; } } } // anonymous namespace TORRENT_TEST(iovec_advance_bufs) { span iov1[10]; span iov2[10]; alloc_iov(iov1, 10); fill_pattern({iov1, 10}); memcpy(iov2, iov1, sizeof(iov1)); span> iov = iov2; // advance iov 13 bytes. Make sure what's left fits pattern 1 shifted // 13 bytes iov = aux::advance_bufs(iov, 13); // make sure what's in int counter = 13; for (auto buf : iov) { for (char v : buf) { TEST_EQUAL(v, static_cast(counter)); ++counter; } } free_iov(iov1, 10); } TORRENT_TEST(iovec_copy_bufs) { span iov1[10]; span iov2[10]; alloc_iov(iov1, 10); fill_pattern({iov1, 10}); // copy exactly 106 bytes from iov1 to iov2 int num_bufs = aux::copy_bufs(span const>(iov1), 106, span>(iov2)); // verify that the first 100 bytes is pattern 1 // and that the remaining bytes are pattern 2 int counter = 0; for (int i = 0; i < num_bufs; ++i) { for (char v : iov2[i]) { TEST_EQUAL(int(v), (counter & 0xff)); ++counter; } } TEST_EQUAL(counter, 106); free_iov(iov1, 10); } #if TORRENT_HAVE_MMAP || TORRENT_HAVE_MAP_VIEW_OF_FILE TORRENT_TEST(mmap_disk_io) { run_test(); } #endif TORRENT_TEST(posix_disk_io) { run_test(); } TORRENT_TEST(pread_disk_io) { run_test(); } namespace { file_storage make_fs() { file_storage fs; fs.add_file(combine_path("readwrite", "1"), 3); fs.add_file(combine_path("readwrite", "2"), 9); fs.add_file(combine_path("readwrite", "3"), 81); fs.add_file(combine_path("readwrite", "4"), 6561); fs.set_piece_length(0x1000); fs.set_num_pieces(aux::calc_num_pieces(fs)); return fs; } struct test_fileop { explicit test_fileop(int stripe_size) : m_stripe_size(stripe_size) {} int operator()(file_index_t const file_index, std::int64_t const file_offset , span buf, storage_error&) { std::size_t offset = size_t(file_offset); if (file_index >= m_file_data.end_index()) { m_file_data.resize(static_cast(file_index) + 1); } std::size_t const write_size = std::size_t(std::min(m_stripe_size, int(buf.size()))); buf = buf.first(int(write_size)); std::vector& file = m_file_data[file_index]; if (offset + write_size > file.size()) file.resize(offset + write_size); std::memcpy(&file[offset], buf.data(), write_size); return int(write_size); } int m_stripe_size; aux::vector, file_index_t> m_file_data; }; struct test_read_fileop { // EOF after size bytes read explicit test_read_fileop(int size) : m_size(size), m_counter(0) {} int operator()(file_index_t, std::int64_t /*file_offset*/ , span buf, storage_error&) { if (buf.size() > m_size) buf = buf.first(m_size); for (char& v : buf) { v = char(m_counter & 0xff); ++m_counter; } m_size -= int(buf.size()); return int(buf.size()); } int m_size; int m_counter; }; struct test_error_fileop { // EOF after size bytes read explicit test_error_fileop(file_index_t error_file) : m_error_file(error_file) {} int operator()(file_index_t const file_index, std::int64_t /*file_offset*/ , span buf, storage_error& ec) { if (m_error_file == file_index) { ec.file(file_index); ec.ec.assign(boost::system::errc::permission_denied , boost::system::generic_category()); ec.operation = operation_t::file_read; return 0; } return int(buf.size()); } file_index_t m_error_file; }; } // anonymous namespace TORRENT_TEST(readwrite_stripe_1) { file_storage fs = make_fs(); test_fileop fop(1); storage_error ec; std::vector buf(std::size_t(fs.total_size())); fill_pattern(buf); int ret = readwrite(fs, span(buf), 0_piece, 0, ec, std::ref(fop)); TEST_EQUAL(ret, fs.total_size()); TEST_EQUAL(fop.m_file_data.size(), 4); TEST_EQUAL(fop.m_file_data[0_file].size(), 3); TEST_EQUAL(fop.m_file_data[1_file].size(), 9); TEST_EQUAL(fop.m_file_data[2_file].size(), 81); TEST_EQUAL(fop.m_file_data[3_file].size(), 6561); TEST_CHECK(check_pattern(fop.m_file_data[0_file], 0)); TEST_CHECK(check_pattern(fop.m_file_data[1_file], 3)); TEST_CHECK(check_pattern(fop.m_file_data[2_file], 3 + 9)); TEST_CHECK(check_pattern(fop.m_file_data[3_file], 3 + 9 + 81)); } TORRENT_TEST(readwrite_single_buffer) { file_storage fs = make_fs(); test_fileop fop(10000000); storage_error ec; std::vector buf(size_t(fs.total_size())); fill_pattern(buf); int ret = readwrite(fs, span(buf), 0_piece, 0, ec, std::ref(fop)); TEST_EQUAL(ret, fs.total_size()); TEST_EQUAL(fop.m_file_data.size(), 4); TEST_EQUAL(fop.m_file_data[0_file].size(), 3); TEST_EQUAL(fop.m_file_data[1_file].size(), 9); TEST_EQUAL(fop.m_file_data[2_file].size(), 81); TEST_EQUAL(fop.m_file_data[3_file].size(), 6561); TEST_CHECK(check_pattern(fop.m_file_data[0_file], 0)); TEST_CHECK(check_pattern(fop.m_file_data[1_file], 3)); TEST_CHECK(check_pattern(fop.m_file_data[2_file], 3 + 9)); TEST_CHECK(check_pattern(fop.m_file_data[3_file], 3 + 9 + 81)); } TORRENT_TEST(readwrite_read) { file_storage fs = make_fs(); test_read_fileop fop(10000000); storage_error ec; std::vector buf(size_t(fs.total_size())); // read everything int ret = readwrite(fs, span(buf), 0_piece, 0, ec, std::ref(fop)); TEST_EQUAL(ret, fs.total_size()); TEST_CHECK(check_pattern(buf, 0)); } TORRENT_TEST(readwrite_read_short) { file_storage fs = make_fs(); test_read_fileop fop(100); storage_error ec; std::vector buf(size_t(fs.total_size())); // read everything int ret = readwrite(fs, span(buf), 0_piece, 0, ec, std::ref(fop)); TEST_EQUAL(static_cast(ec.file()), 3); TEST_EQUAL(ret, 100); buf.resize(100); TEST_CHECK(check_pattern(buf, 0)); } TORRENT_TEST(readwrite_error) { file_storage fs = make_fs(); test_error_fileop fop(2_file); storage_error ec; std::vector buf(size_t(fs.total_size())); // read everything int ret = readwrite(fs, span(buf), 0_piece, 0, ec, std::ref(fop)); TEST_EQUAL(ret, 12); TEST_EQUAL(static_cast(ec.file()), 2); TEST_CHECK(ec.operation == operation_t::file_read); TEST_EQUAL(ec.ec, boost::system::errc::permission_denied); std::printf("error: %s\n", ec.ec.message().c_str()); } TORRENT_TEST(readwrite_zero_size_files) { file_storage fs; fs.add_file(combine_path("readwrite", "1"), 3); fs.add_file(combine_path("readwrite", "2"), 0); fs.add_file(combine_path("readwrite", "3"), 81); fs.add_file(combine_path("readwrite", "4"), 0); fs.add_file(combine_path("readwrite", "5"), 6561); fs.set_piece_length(0x1000); fs.set_num_pieces(aux::calc_num_pieces(fs)); test_read_fileop fop(10000000); storage_error ec; std::vector buf(size_t(fs.total_size())); // read everything int ret = readwrite(fs, span(buf), 0_piece, 0, ec, std::ref(fop)); TEST_EQUAL(ret, fs.total_size()); TEST_CHECK(check_pattern(buf, 0)); } template void test_move_storage_to_self() { // call move_storage with the path to the existing storage. should be a no-op std::string const save_path = current_working_directory(); std::string const test_path = complete("temp_storage"); delete_dirs(test_path); aux::session_settings set; std::vector buf; typename file_pool_type::type fp; io_context ios; auto [s, info] = setup_torrent(fp, buf, save_path, set); span const b = {&buf[0], 4}; storage_error se; TEST_EQUAL(se.ec, boost::system::errc::success); write(s, set, b, 1_piece, 0, aux::open_mode::write, se); TEST_CHECK(exists(combine_path(test_path, combine_path("folder2", "test3.tmp")))); TEST_CHECK(exists(combine_path(test_path, combine_path("_folder3", "test4.tmp")))); TEST_EQUAL(se.ec, boost::system::errc::success); s->move_storage(save_path, move_flags_t::always_replace_files, se); TEST_EQUAL(se.ec, boost::system::errc::success); std::cerr << "file: " << se.file() << '\n'; std::cerr << "op: " << int(se.operation) << '\n'; std::cerr << "ec: " << se.ec.message() << '\n'; TEST_CHECK(exists(test_path)); TEST_CHECK(exists(combine_path(test_path, combine_path("folder2", "test3.tmp")))); TEST_CHECK(exists(combine_path(test_path, combine_path("_folder3", "test4.tmp")))); } template void test_move_storage_into_self() { std::string const save_path = current_working_directory(); delete_dirs("temp_storage"); aux::session_settings set; std::vector buf; typename file_pool_type::type fp; io_context ios; auto [s, info] = setup_torrent(fp, buf, save_path, set); span const b = {&buf[0], 4}; storage_error se; write(s, set, b, 2_piece, 0, aux::open_mode::write, se); std::string const test_path = combine_path(save_path, combine_path("temp_storage", "folder1")); s->move_storage(test_path, move_flags_t::always_replace_files, se); TEST_EQUAL(se.ec, boost::system::errc::success); TEST_CHECK(exists(combine_path(test_path, combine_path("temp_storage" , combine_path("folder1", "test2.tmp"))))); // these directories and files are created up-front because they are empty files TEST_CHECK(exists(combine_path(test_path, combine_path("temp_storage" , combine_path("folder2", "test3.tmp"))))); TEST_CHECK(exists(combine_path(test_path, combine_path("temp_storage" , combine_path("_folder3", "test4.tmp"))))); } template void test_move_storage_reset(move_flags_t const flags) { std::string const save_path = current_working_directory(); std::string const test_path = complete("temp_storage2"); delete_dirs(test_path); aux::session_settings set; file_storage fs; std::vector buf; typename file_pool_type::type fp; io_context ios; auto [s, ifno] = setup_torrent(fp, buf, save_path, set); span const b = {&buf[0], 4}; storage_error se; TEST_EQUAL(se.ec, boost::system::errc::success); write(s, set, b, 1_piece, 0, aux::open_mode::write, se); std::string const root = combine_path(save_path, "temp_storage"); TEST_CHECK(exists(combine_path(root, combine_path("folder2", "test3.tmp")))); TEST_CHECK(exists(combine_path(root, combine_path("_folder3", "test4.tmp")))); std::string const root2 = combine_path(test_path, "temp_storage"); TEST_CHECK(!exists(combine_path(root2, combine_path("folder2", "test3.tmp")))); TEST_CHECK(!exists(combine_path(root2, combine_path("_folder3", "test4.tmp")))); TEST_EQUAL(se.ec, boost::system::errc::success); std::string new_path; status_t ret; std::tie(ret, new_path) = s->move_storage(test_path, flags, se); TEST_EQUAL(new_path, test_path); TEST_EQUAL(se.ec, boost::system::errc::success); std::cerr << "file: " << se.file() << '\n'; std::cerr << "op: " << int(se.operation) << '\n'; std::cerr << "ec: " << se.ec.message() << '\n'; // the root directory is created, but none of the files are moved TEST_CHECK(exists(test_path)); TEST_CHECK(!exists(combine_path(root2, combine_path("folder2", "test3.tmp")))); TEST_CHECK(!exists(combine_path(root2, combine_path("_folder3", "test4.tmp")))); } #if TORRENT_HAVE_MMAP || TORRENT_HAVE_MAP_VIEW_OF_FILE TORRENT_TEST(move_default_storage_to_self) { test_move_storage_to_self(); } TORRENT_TEST(move_default_storage_into_self) { test_move_storage_into_self(); } TORRENT_TEST(move_default_storage_reset) { test_move_storage_reset(move_flags_t::reset_save_path); test_move_storage_reset(move_flags_t::reset_save_path_unchecked); } #endif TORRENT_TEST(move_posix_storage_to_self) { test_move_storage_to_self(); } TORRENT_TEST(move_posix_storage_into_self) { test_move_storage_into_self(); } TORRENT_TEST(move_posix_storage_reset) { test_move_storage_reset(move_flags_t::reset_save_path); test_move_storage_reset(move_flags_t::reset_save_path_unchecked); } TORRENT_TEST(move_pread_storage_to_self) { test_move_storage_to_self(); } TORRENT_TEST(move_pread_storage_into_self) { test_move_storage_into_self(); } TORRENT_TEST(move_pread_storage_reset) { test_move_storage_reset(move_flags_t::reset_save_path); test_move_storage_reset(move_flags_t::reset_save_path_unchecked); } TORRENT_TEST(storage_paths_string_pooling) { file_storage file_storage; file_storage.add_file(combine_path("test_storage", "root.txt"), 0x4000); file_storage.add_file(combine_path("test_storage", combine_path("sub", "test1.txt")), 0x4000); file_storage.add_file(combine_path("test_storage", combine_path("sub", "test2.txt")), 0x4000); file_storage.add_file(combine_path("test_storage", combine_path("sub", "test3.txt")), 0x4000); // "sub" paths should point to same string item, so paths.size() must not grow TEST_CHECK(file_storage.paths().size() <= 2); } #if TORRENT_HAVE_MMAP || TORRENT_HAVE_MAP_VIEW_OF_FILE TORRENT_TEST(dont_move_intermingled_files) { std::string const save_path = complete("save_path_1"); delete_dirs(combine_path(save_path, "temp_storage")); std::string const test_path = complete("save_path_2"); delete_dirs(combine_path(test_path, "temp_storage")); aux::session_settings set; std::vector buf; typename file_pool_type::type fp; io_context ios; auto [s, info] = setup_torrent(fp, buf, save_path, set); span b = {&buf[0], 4}; storage_error se; s->write(set, b, 2_piece, 0, aux::open_mode::write, disk_job_flags_t{}, se); error_code ec; create_directory(combine_path(save_path, combine_path("temp_storage" , combine_path("_folder3", "alien_folder1"))), ec); TEST_EQUAL(ec, boost::system::errc::success); ofstream(combine_path(save_path, combine_path("temp_storage", "alien1.tmp")).c_str()); ofstream(combine_path(save_path, combine_path("temp_storage", combine_path("folder1", "alien2.tmp"))).c_str()); s->move_storage(test_path, move_flags_t::always_replace_files, se); TEST_EQUAL(se.ec, boost::system::errc::success); // torrent files moved to new place TEST_CHECK(exists(combine_path(test_path, combine_path("temp_storage" , combine_path("folder1", "test2.tmp"))))); // these directories and files are created up-front because they are empty files TEST_CHECK(exists(combine_path(test_path, combine_path("temp_storage" , combine_path("folder2", "test3.tmp"))))); TEST_CHECK(exists(combine_path(test_path, combine_path("temp_storage" , combine_path("_folder3", "test4.tmp"))))); // intermingled files and directories are still in old place TEST_CHECK(exists(combine_path(save_path, combine_path("temp_storage" , "alien1.tmp")))); TEST_CHECK(!exists(combine_path(test_path, combine_path("temp_storage" , "alien1.tmp")))); TEST_CHECK(exists(combine_path(save_path, combine_path("temp_storage" , combine_path("folder1", "alien2.tmp"))))); TEST_CHECK(!exists(combine_path(test_path, combine_path("temp_storage" , combine_path("folder1", "alien2.tmp"))))); TEST_CHECK(exists(combine_path(save_path, combine_path("temp_storage" , combine_path("_folder3", "alien_folder1"))))); TEST_CHECK(!exists(combine_path(test_path, combine_path("temp_storage" , combine_path("_folder3", "alien_folder1"))))); } #endif namespace { void sync(lt::io_context& ioc, int& outstanding) { while (outstanding > 0) { ioc.restart(); ioc.run_one_for(lt::milliseconds(100)); } } template void test_unaligned_read(lt::disk_io_constructor_type constructor, Fun fun) { lt::io_context ioc; lt::counters cnt; lt::settings_pack pack; pack.set_int(lt::settings_pack::aio_threads, 1); pack.set_int(lt::settings_pack::file_pool_size, 2); std::unique_ptr disk_io = constructor(ioc, pack, cnt); lt::file_storage fs; fs.add_file("test", lt::default_block_size * 2); fs.set_num_pieces(1); fs.set_piece_length(lt::default_block_size * 2); std::string const save_path = complete("save_path"); delete_dirs(combine_path(save_path, "test")); lt::aux::vector prios; lt::renamed_files rf; lt::storage_params params(fs , rf , save_path , {} , lt::storage_mode_sparse , prios , lt::sha1_hash("01234567890123456789") , true // v1-hashes , true // v2-hashes ); lt::storage_holder t = disk_io->new_torrent(params, {}); int outstanding = 0; lt::add_torrent_params atp; disk_io->async_check_files(t, &atp, lt::aux::vector{} , [&](lt::status_t, lt::storage_error const&) { --outstanding; }); ++outstanding; disk_io->submit_jobs(); sync(ioc, outstanding); fun(disk_io.get(), t, ioc, outstanding); t.reset(); disk_io->abort(true); } struct write_handler { write_handler(int& outstanding) : m_out(&outstanding) {} void operator()(lt::storage_error const& ec) const { --(*m_out); if (ec) std::cout << "async_write failed " << ec.ec.message() << '\n'; TEST_CHECK(!ec); } int* m_out; }; struct read_handler { // disk_buffer_holder doesn't expose a size, so the caller is // responsible for sizing the read to default_block_size and // supplying that many expected bytes. read_handler(int& outstanding, char const* expected) : m_out(&outstanding) , m_exp(expected) {} void operator()(lt::disk_buffer_holder h, lt::storage_error const& ec) const { --(*m_out); if (ec) std::cout << "async_read failed " << ec.ec.message() << '\n'; TEST_CHECK(!ec); TEST_CHECK(lt::span(m_exp, lt::default_block_size) == lt::span(h.data(), lt::default_block_size)); } int* m_out; char const* m_exp; }; void both_sides_from_store_buffer(lt::disk_interface* disk_io, lt::storage_holder const& t, lt::io_context& ioc, int& outstanding) { std::vector write_buffer(lt::default_block_size * 2); aux::random_bytes(write_buffer); lt::peer_request const req0{0_piece, 0, lt::default_block_size}; lt::peer_request const req1{0_piece, lt::default_block_size, lt::default_block_size}; // this is the unaligned read request lt::peer_request const req2{0_piece, lt::default_block_size / 2, lt::default_block_size}; std::vector const expected_buffer(write_buffer.begin() + req2.start , write_buffer.begin() + req2.start + req2.length); ++outstanding; disk_io->async_write(t, req0, write_buffer.data(), {}, write_handler(outstanding)); ++outstanding; disk_io->async_write(t, req1, write_buffer.data() + lt::default_block_size, {}, write_handler(outstanding)); ++outstanding; disk_io->async_read(t, req2, read_handler(outstanding, expected_buffer.data())); disk_io->submit_jobs(); sync(ioc, outstanding); } void first_side_from_store_buffer(lt::disk_interface* disk_io, lt::storage_holder const& t, lt::io_context& ioc, int& outstanding) { std::vector write_buffer(lt::default_block_size * 2); aux::random_bytes(write_buffer); lt::peer_request const req0{0_piece, 0, lt::default_block_size}; lt::peer_request const req1{0_piece, lt::default_block_size, lt::default_block_size}; // this is the unaligned read request lt::peer_request const req2{0_piece, lt::default_block_size / 2, lt::default_block_size}; std::vector const expected_buffer(write_buffer.begin() + req2.start , write_buffer.begin() + req2.start + req2.length); ++outstanding; disk_io->async_write(t, req0, write_buffer.data(), {}, write_handler(outstanding)); disk_io->submit_jobs(); sync(ioc, outstanding); ++outstanding; disk_io->async_write(t, req1, write_buffer.data() + lt::default_block_size, {}, write_handler(outstanding)); ++outstanding; disk_io->async_read(t, req2, read_handler(outstanding, expected_buffer.data())); disk_io->submit_jobs(); sync(ioc, outstanding); } void second_side_from_store_buffer(lt::disk_interface* disk_io, lt::storage_holder const& t, lt::io_context& ioc, int& outstanding) { std::vector write_buffer(lt::default_block_size * 2); aux::random_bytes(write_buffer); lt::peer_request const req0{0_piece, 0, lt::default_block_size}; lt::peer_request const req1{0_piece, lt::default_block_size, lt::default_block_size}; // this is the unaligned read request lt::peer_request const req2{0_piece, lt::default_block_size / 2, lt::default_block_size}; std::vector const expected_buffer(write_buffer.begin() + req2.start , write_buffer.begin() + req2.start + req2.length); ++outstanding; disk_io->async_write(t, req1, write_buffer.data() + lt::default_block_size, {}, write_handler(outstanding)); disk_io->submit_jobs(); sync(ioc, outstanding); ++outstanding; disk_io->async_write(t, req0, write_buffer.data(), {}, write_handler(outstanding)); ++outstanding; disk_io->async_read(t, req2, read_handler(outstanding, expected_buffer.data())); disk_io->submit_jobs(); sync(ioc, outstanding); } void none_from_store_buffer(lt::disk_interface* disk_io, lt::storage_holder const& t, lt::io_context& ioc, int& outstanding) { std::vector write_buffer(lt::default_block_size * 2); aux::random_bytes(write_buffer); lt::peer_request const req0{0_piece, 0, lt::default_block_size}; lt::peer_request const req1{0_piece, lt::default_block_size, lt::default_block_size}; // this is the unaligned read request lt::peer_request const req2{0_piece, lt::default_block_size / 2, lt::default_block_size}; std::vector const expected_buffer(write_buffer.begin() + req2.start , write_buffer.begin() + req2.start + req2.length); ++outstanding; disk_io->async_write(t, req0, write_buffer.data(), {}, write_handler(outstanding)); ++outstanding; disk_io->async_write(t, req1, write_buffer.data() + lt::default_block_size, {}, write_handler(outstanding)); disk_io->submit_jobs(); sync(ioc, outstanding); ++outstanding; disk_io->async_read(t, req2, read_handler(outstanding, expected_buffer.data())); disk_io->submit_jobs(); sync(ioc, outstanding); } } #if TORRENT_HAVE_MMAP || TORRENT_HAVE_MAP_VIEW_OF_FILE TORRENT_TEST(mmap_unaligned_read_both_store_buffer) { test_unaligned_read(lt::mmap_disk_io_constructor, both_sides_from_store_buffer); test_unaligned_read(lt::mmap_disk_io_constructor, first_side_from_store_buffer); test_unaligned_read(lt::mmap_disk_io_constructor, second_side_from_store_buffer); test_unaligned_read(lt::mmap_disk_io_constructor, none_from_store_buffer); } #endif TORRENT_TEST(posix_unaligned_read_both_store_buffer) { test_unaligned_read(lt::posix_disk_io_constructor, both_sides_from_store_buffer); test_unaligned_read(lt::posix_disk_io_constructor, first_side_from_store_buffer); test_unaligned_read(lt::posix_disk_io_constructor, second_side_from_store_buffer); test_unaligned_read(lt::posix_disk_io_constructor, none_from_store_buffer); } TORRENT_TEST(iovec_bufs) { span iov[10]; for (int i = 1; i < 10; ++i) { alloc_iov(iov, i); int expected_size = 0; for (int k = 0; k < i; ++k) expected_size += i * (k + 1); TEST_EQUAL(aux::bufs_size(span const>(&iov[0], i)), expected_size); free_iov(iov, i); } } using part_file_flag_t = lt::flags::bitfield_flag; constexpr part_file_flag_t custom_path = 0_bit; template void test_part_file(lt::storage_mode_t const storage_mode, part_file_flag_t const flags) { static int test_index = 0; std::cerr << "test_part_file " << test_index << " " << typeid(StorageType).name() << " storage_mode:" << storage_mode << " " << ((flags & custom_path ) ? " custom_path" : "") << std::endl; std::vector piece0 = new_piece(17 + 612); delete_dirs("temp_storage"); std::vector fs; fs.emplace_back("temp_storage/test1.tmp", 17); fs.emplace_back("temp_storage/test2.tmp", 612); lt::create_torrent t(std::move(fs), piece_size, create_torrent::v1_only); TEST_CHECK(t.num_pieces() == 1); t.set_hash(0_piece, hasher(piece0).final()); std::shared_ptr info = load_torrent_buffer(bencode(t.generate())).ti; aux::session_settings set; // avoid having two storages use the same files typename file_pool_type::type fp; boost::asio::io_context ios; aux::vector priorities = {1_pri, 0_pri}; std::string const cwd = current_working_directory(); std::string download_dir = combine_path(cwd, "part_file_test_" + std::to_string(test_index)); std::string const part_file_dir = combine_path(download_dir, "part_file_dir"); ++test_index; renamed_files rf; storage_params p( info->layout(), rf, download_dir, (flags & custom_path) ? "part_file_dir" : "", storage_mode, priorities, sha1_hash{}, info->v1(), info->v2() ); auto s = make_storage(p, fp); storage_error ec; s->initialize(set, ec); TEST_CHECK(!ec); if (ec) print_error("initialize", 0, ec); int ret = 0; // write piece 1 span iov = span(piece0); ret = write(s, set, iov, 0_piece, 0, aux::open_mode::write, ec); TEST_EQUAL(ret, int(iov.size())); if (ret != int(iov.size())) print_error("write", ret, ec); release_files(s, ec); // has_any_file should return true since we wrote data { storage_error haf_ec; TEST_CHECK(s->has_any_file(haf_ec)); TEST_CHECK(!haf_ec); } // make sure the part file is stored in the expected location std::string const part_file_name = "." + aux::to_hex(p.info_hash) + ".parts"; if (flags & custom_path) { TEST_CHECK(exists(combine_path(part_file_dir, part_file_name))); TEST_CHECK(!exists(combine_path(download_dir, part_file_name))); } else { TEST_CHECK(!exists(combine_path(part_file_dir, part_file_name))); TEST_CHECK(exists(combine_path(download_dir, part_file_name))); } // now move the torrent and make sure the part file is where it's expected to be std::string const new_download_dir = download_dir + "_new"; std::string const new_part_file_dir = combine_path(new_download_dir, "part_file_dir"); storage_error se; s->move_storage(new_download_dir, move_flags_t::always_replace_files, se); TEST_CHECK(!se); if (flags & custom_path) { TEST_CHECK(exists(combine_path(new_part_file_dir, part_file_name))); TEST_CHECK(!exists(combine_path(download_dir, part_file_name))); TEST_CHECK(!exists(combine_path(new_download_dir, part_file_name))); } else { TEST_CHECK(!exists(combine_path(new_part_file_dir, part_file_name))); TEST_CHECK(!exists(combine_path(download_dir, part_file_name))); TEST_CHECK(exists(combine_path(new_download_dir, part_file_name))); } // delete_files should remove the part file from the correct location s->delete_files(session::delete_files, se); if (se) print_error("delete_files", 0, se); TEST_CHECK(!se); if (flags & custom_path) { TEST_CHECK(!exists(combine_path(new_part_file_dir, part_file_name))); } else { TEST_CHECK(!exists(combine_path(new_download_dir, part_file_name))); } // has_any_file should return false after all files have been deleted { storage_error haf_ec; TEST_CHECK(!s->has_any_file(haf_ec)); TEST_CHECK(!haf_ec); } } #if TORRENT_HAVE_MMAP || TORRENT_HAVE_MAP_VIEW_OF_FILE TORRENT_TEST(mmap_disk_io_part_file) { for (auto storage_mode : { storage_mode_sparse, storage_mode_allocate }) { for (auto flags : {part_file_flag_t{}, custom_path}) { test_part_file(storage_mode, flags); } } } #endif TORRENT_TEST(pread_disk_io_part_file) { for (auto storage_mode : { storage_mode_sparse, storage_mode_allocate }) { for (auto flags : {part_file_flag_t{}, custom_path}) { test_part_file(storage_mode, flags); } } } TORRENT_TEST(posix_disk_io_part_file) { for (auto storage_mode : { storage_mode_sparse, storage_mode_allocate }) { for (auto flags : {part_file_flag_t{}, custom_path}) { test_part_file(storage_mode, flags); } } } // Test the vec-write path (pread_storage::write(span const>)) // when a piece overlaps a 0-priority file. mmap_storage and posix_storage use // the single-buffer readwrite() path so this overload is pread_storage-only. // // file_defs is a list of (block_count, priority) pairs. All files are packed // into a single piece so that the write covers all of them at once. static void test_write_vec_partfile( std::vector> const& file_defs , lt::storage_mode_t const storage_mode) { int const block = 0x4000; // 16 KiB int total_blocks = 0; for (auto const& [nblocks, _prio] : file_defs) total_blocks += nblocks; int const ps = block * total_blocks; // one piece covers all files std::vector flist; aux::vector priorities; for (std::size_t i = 0; i < file_defs.size(); ++i) { auto const [nblocks, prio] = file_defs[i]; flist.emplace_back("vec_test/f" + std::to_string(i) + ".tmp" , std::int64_t(block) * nblocks); priorities.push_back(prio); } lt::create_torrent t(std::move(flist), ps, lt::create_torrent::v1_only); TEST_CHECK(t.num_pieces() == 1); std::vector> blks(static_cast(total_blocks)); std::vector full_piece; for (auto& b : blks) { b = new_piece(block); full_piece.insert(full_piece.end(), b.begin(), b.end()); } t.set_hash(0_piece, hasher(full_piece).final()); std::shared_ptr info = load_torrent_buffer(bencode(t.generate())).ti; aux::session_settings set; file_pool_type::type fp; std::string const cwd = current_working_directory(); std::string const download_dir = combine_path(cwd, "pread_vec_partfile_test"); delete_dirs(download_dir); renamed_files rf; storage_params p( info->layout(), rf, download_dir, {}, storage_mode, priorities, sha1_hash{}, info->v1(), info->v2() ); auto s = make_storage(p, fp); storage_error ec; s->initialize(set, ec); TEST_CHECK(!ec); if (ec) print_error("initialize", 0, ec); // write piece 0 as individual block-sized buffers via the vec overload. // any 0-priority file spanning more than one block causes bufs.size() > 1 // in the part-file branch of the write lambda. std::vector> buf_spans; buf_spans.reserve(std::size_t(total_blocks)); for (auto const& b : blks) buf_spans.emplace_back(b); span const> const vec_bufs(buf_spans.data() , static_cast(buf_spans.size())); int const wret = s->write(set, vec_bufs, 0_piece, 0 , aux::open_mode::write, disk_job_flags_t{}, ec); TEST_EQUAL(wret, ps); TEST_CHECK(!ec); if (ec) print_error("write", wret, ec); std::vector read_buf(static_cast(ps)); int const rret = s->read(set, read_buf, 0_piece, 0 , aux::open_mode::read_only, disk_job_flags_t{}, ec); TEST_EQUAL(rret, ps); TEST_CHECK(!ec); if (ec) print_error("read", rret, ec); TEST_CHECK(read_buf == full_piece); s->delete_files(session::delete_files, ec); if (ec) print_error("delete_files", 0, ec); } TORRENT_TEST(pread_write_vec_partfile_multi_buf) { for (auto const sm : {storage_mode_sparse, storage_mode_allocate}) { // 0-priority file (2 blocks) at the start of the piece test_write_vec_partfile({{2, 0_pri}, {2, 1_pri}}, sm); // 0-priority file (2 blocks) at the end of the piece test_write_vec_partfile({{2, 1_pri}, {2, 0_pri}}, sm); // 0-priority file (2 blocks) in the middle, normal files on both sides test_write_vec_partfile({{1, 1_pri}, {2, 0_pri}, {1, 1_pri}}, sm); } }