/* Copyright (c) 2005, 2007-2010, 2012-2022, Arvid Norberg Copyright (c) 2016, 2018, 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. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "test.hpp" #include "setup_transfer.hpp" #include "test_utils.hpp" #include "settings.hpp" #include "libtorrent/mmap_storage.hpp" #include "libtorrent/aux_/posix_storage.hpp" #include "libtorrent/aux_/file_view_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/random.hpp" #include "libtorrent/mmap_disk_io.hpp" #include "libtorrent/posix_disk_io.hpp" #include "libtorrent/flags.hpp" #include #include // for bind #include #include "libtorrent/aux_/disable_warnings_push.hpp" #include #include "libtorrent/aux_/disable_warnings_pop.hpp" using namespace std::placeholders; using namespace lt; #if ! TORRENT_HAVE_MMAP && ! TORRENT_HAVE_MAP_VIEW_OF_FILE namespace libtorrent { namespace aux { struct file_view_pool {}; } } #endif namespace { #if TORRENT_HAVE_MMAP || TORRENT_HAVE_MAP_VIEW_OF_FILE using lt::mmap_storage; #endif using lt::aux::posix_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: " << static_cast(status); if ((status & lt::status_t::oversized_file) != status_t{}) { std::cout << " oversized file(s) - "; *oversized = true; } else { *oversized = false; } switch (status & ~lt::status_t::mask) { case lt::status_t::no_error: std::cout << " success" << std::endl; break; case lt::status_t::fatal_disk_error: std::cout << " disk error: " << error.ec.message() << " file: " << error.file() << std::endl; break; case lt::status_t::need_full_check: std::cout << " need full check" << std::endl; break; case lt::status_t::file_exist: std::cout << " file exist" << std::endl; break; case lt::status_t::mask: case lt::status_t::oversized_file: break; } 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(); std::cout << time_now_string() << " done: " << done << std::endl; } } std::shared_ptr setup_torrent_info(file_storage& fs , std::vector& buf) { fs.add_file(combine_path("temp_storage", "test1.tmp"), 0x8000); fs.add_file(combine_path("temp_storage", combine_path("folder1", "test2.tmp")), 0x8000); fs.add_file(combine_path("temp_storage", combine_path("folder2", "test3.tmp")), 0); fs.add_file(combine_path("temp_storage", combine_path("_folder3", "test4.tmp")), 0); fs.add_file(combine_path("temp_storage", combine_path("_folder3", combine_path("subfolder", "test5.tmp"))), 0x8000); lt::create_torrent t(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); bencode(std::back_inserter(buf), t.generate()); error_code ec; auto info = std::make_shared(buf, ec, from_span); if (ec) { std::printf("torrent_info constructor failed: %s\n" , ec.message().c_str()); throw system_error(ec); } return info; } // 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 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 setup_torrent(file_storage& fs , FilePool& fp , std::vector& buf , std::string const& test_path , aux::session_settings& set) { std::shared_ptr info = setup_torrent_info(fs, buf); aux::vector priorities; sha1_hash info_hash; storage_params p{ fs, nullptr, test_path, storage_mode_allocate, priorities, info_hash }; 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; } #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&) {} 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 , file_storage& fs , lt::storage_mode_t storage_mode) { 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; sha1_hash info_hash; std::string const cwd = current_working_directory(); storage_params p{ fs, nullptr, cwd, storage_mode, priorities, info_hash }; 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"); file_storage fs; std::vector buf; typename file_pool_type::type fp; io_context ios; aux::session_settings set; auto s = setup_torrent(fs, 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"); file_storage fs; std::vector buf; typename file_pool_type::type fp; io_context ios; aux::session_settings set; auto s = setup_torrent(fs, fp, buf, test_path, set); // 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->files().file_path(0_file), "new_filename"); } 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(); file_storage fs; std::vector buf; typename file_pool_type::type fp; io_context ios; aux::session_settings set; std::shared_ptr info = setup_torrent_info(fs, buf); aux::vector priorities{ lt::dont_download, lt::default_priority, lt::default_priority, lt::default_priority, lt::default_priority, }; sha1_hash info_hash; storage_params p{ fs, nullptr, test_path, storage_mode_allocate, priorities, info_hash }; 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)); } } } 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) { std::string const test_path = current_working_directory(); std::shared_ptr info; error_code ec; constexpr int piece_size_check = 16 * 1024; delete_dirs("temp_storage"); file_storage fs; fs.add_file("temp_storage/test1.tmp", piece_size_check); fs.add_file("temp_storage/test2.tmp", piece_size_check * 2); fs.add_file("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(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::vector buf; bencode(std::back_inserter(buf), t.generate()); info = std::make_shared(buf, ec, from_span); aux::session_settings set; boost::asio::io_context ios; counters cnt; aux::session_settings sett; sett.set_int(settings_pack::aio_threads, 1); 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{}); sha1_hash info_hash; storage_params p{ fs, nullptr, test_path, (flags & sparse) ? storage_mode_sparse : storage_mode_allocate, priorities, info_hash }; 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); } 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::shared_ptr info; 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"); file_storage fs; fs.add_file("temp_storage/test1.tmp", 17); fs.add_file("temp_storage/test2.tmp", 612); fs.add_file("temp_storage/test3.tmp", 0); fs.add_file("temp_storage/test4.tmp", 0); fs.add_file("temp_storage/test5.tmp", 3253); fs.add_file("temp_storage/test6.tmp", 841); int const last_file_size = 4 * int(piece_size) - int(fs.total_size()); fs.add_file("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(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::vector buf; bencode(std::back_inserter(buf), t.generate()); info = std::make_shared(buf, from_span); // run_storage_tests writes piece 0, 1 and 2. not 3 run_storage_tests(info, fs, storage_mode_sparse); // 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"); } #if TORRENT_HAVE_MMAP || TORRENT_HAVE_MAP_VIEW_OF_FILE TORRENT_TEST(check_files_sparse_mmap) { test_check_files(sparse | zero_prio, lt::mmap_disk_io_constructor); } TORRENT_TEST(check_files_oversized_mmap_zero_prio) { test_check_files(sparse | zero_prio | test_oversized, lt::mmap_disk_io_constructor); } TORRENT_TEST(check_files_oversized_mmap) { test_check_files(sparse | test_oversized, lt::mmap_disk_io_constructor); } TORRENT_TEST(check_files_allocate_mmap) { test_check_files(zero_prio, lt::mmap_disk_io_constructor); } TORRENT_TEST(test_pre_allocate_mmap) { test_pre_allocate(); } #endif TORRENT_TEST(check_files_sparse_posix) { test_check_files(sparse | zero_prio, lt::posix_disk_io_constructor); } TORRENT_TEST(check_files_oversized_zero_prio_posix) { test_check_files(sparse | zero_prio | test_oversized, lt::posix_disk_io_constructor); } TORRENT_TEST(check_files_oversized_posix) { test_check_files(sparse | test_oversized, lt::posix_disk_io_constructor); } TORRENT_TEST(check_files_allocate_posix) { test_check_files(zero_prio, lt::posix_disk_io_constructor); } // posix_storage doesn't support pre-allocating files on non-windows /* 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()); } 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()); std::shared_ptr t = ::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; p.ti = std::make_shared(std::cref(*t)); 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(); 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 resume_data; bencode(std::back_inserter(resume_data), 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 = std::make_shared(std::cref(*t)); 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 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; file_storage fs; std::shared_ptr info = setup_torrent_info(fs, buf); 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() + "__")); 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()); std::shared_ptr t = ::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 = std::make_shared(std::cref(*t)); 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 resume_data; bencode(std::back_inserter(resume_data), 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 = std::make_shared(std::cref(*t)); 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; } } 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; } } // anonymous namespace #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(); } 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, 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, 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, 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, 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, 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, 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; file_storage fs; std::vector buf; typename file_pool_type::type fp; io_context ios; auto s = setup_torrent(fs, 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; file_storage fs; std::vector buf; typename file_pool_type::type fp; io_context ios; auto s = setup_torrent(fs, 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 = setup_torrent(fs, 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(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; file_storage fs; std::vector buf; typename file_pool_type::type fp; io_context ios; auto s = setup_torrent(fs, 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.run_one(); ioc.restart(); } } 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::storage_params params(fs, nullptr , save_path , lt::storage_mode_sparse , prios , lt::sha1_hash("01234567890123456789")); 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 { read_handler(int& outstanding, lt::span 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(m_exp == lt::span(h.data(), h.size())); } int* m_out; lt::span 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)); 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)); 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)); 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)); 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); }