/* vim:set ts=2 sw=2 sts=2 et: */ /** * \author Marcus Holland-Moritz (github@mhxnet.de) * \copyright Copyright (c) Marcus Holland-Moritz * * This file is part of dwarfs. * * dwarfs is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * dwarfs is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with dwarfs. If not, see . */ #include #include #include #include #include #include #include #include #include // This needs to be included *after* gtest.h #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "filter_test_data.h" #include "loremipsum.h" #include "mmap_mock.h" #include "test_helpers.h" #include "test_logger.h" using namespace dwarfs; namespace fs = std::filesystem; namespace { std::string const default_file_hash_algo{"xxh3-128"}; // TODO: jeeeez, this is ugly :/ std::string build_dwarfs(logger& lgr, std::shared_ptr input, std::string const& compression, writer::segmenter::config const& cfg = writer::segmenter::config(), writer::scanner_options const& options = writer::scanner_options(), writer::writer_progress* prog = nullptr, std::shared_ptr ftd = nullptr, std::optional> input_list = std::nullopt, std::unique_ptr filter = nullptr) { // force multithreading thread_pool pool(lgr, *input, "worker", 4); std::unique_ptr local_prog; if (!prog) { local_prog = std::make_unique(); prog = local_prog.get(); } // TODO: ugly hack :-) writer::segmenter_factory::config sf_cfg; sf_cfg.block_size_bits = cfg.block_size_bits; sf_cfg.blockhash_window_size.set_default(cfg.blockhash_window_size); sf_cfg.window_increment_shift.set_default(cfg.window_increment_shift); sf_cfg.max_active_blocks.set_default(cfg.max_active_blocks); sf_cfg.bloom_filter_size.set_default(cfg.bloom_filter_size); writer::segmenter_factory sf(lgr, *prog, sf_cfg); writer::entry_factory ef; writer::scanner s(lgr, pool, sf, ef, *input, options); if (ftd) { s.add_filter(std::make_unique(ftd)); s.add_transformer(std::make_unique(ftd)); } if (filter) { s.add_filter(std::move(filter)); } std::ostringstream oss; block_compressor bc(compression); writer::filesystem_writer fsw(oss, lgr, pool, *prog); fsw.add_default_compressor(bc); s.scan(fsw, std::filesystem::path("/"), *prog, input_list); return oss.str(); } void basic_end_to_end_test( std::string const& compressor, unsigned block_size_bits, writer::fragment_order_mode file_order, bool with_devices, bool with_specials, bool set_uid, bool set_gid, bool set_time, bool keep_all_times, bool enable_nlink, bool pack_chunk_table, bool pack_directories, bool pack_shared_files_table, bool pack_names, bool pack_names_index, bool pack_symlinks, bool pack_symlinks_index, bool plain_names_table, bool plain_symlinks_table, bool access_fail, size_t readahead, std::optional file_hash_algo) { writer::segmenter::config cfg; writer::scanner_options options; cfg.blockhash_window_size = 10; cfg.block_size_bits = block_size_bits; writer::fragment_order_options order_opts; order_opts.mode = file_order; options.file_hash_algorithm = file_hash_algo; options.with_devices = with_devices; options.with_specials = with_specials; options.inode.fragment_order.set_default(order_opts); options.keep_all_times = keep_all_times; options.pack_chunk_table = pack_chunk_table; options.pack_directories = pack_directories; options.pack_shared_files_table = pack_shared_files_table; options.pack_names = pack_names; options.pack_names_index = pack_names_index; options.pack_symlinks = pack_symlinks; options.pack_symlinks_index = pack_symlinks_index; options.force_pack_string_tables = true; options.plain_names_table = plain_names_table; options.plain_symlinks_table = plain_symlinks_table; if (set_uid) { options.uid = 0; } if (set_gid) { options.gid = 0; } if (set_time) { options.timestamp = 4711; } test::test_logger lgr; auto input = test::os_access_mock::create_test_instance(); if (access_fail) { input->set_access_fail("/somedir/ipsum.py"); } writer::writer_progress wprog; auto ftd = std::make_shared(); auto fsimage = build_dwarfs(lgr, input, compressor, cfg, options, &wprog, ftd); EXPECT_EQ(14, ftd->filter_calls.size()); EXPECT_EQ(15, ftd->transform_calls.size()); auto image_size = fsimage.size(); auto mm = std::make_shared(std::move(fsimage)); bool similarity = file_order == writer::fragment_order_mode::SIMILARITY || file_order == writer::fragment_order_mode::NILSIMSA; size_t const num_fail_empty = access_fail ? 1 : 0; auto& prog = wprog.get_internal(); EXPECT_EQ(8, prog.files_found); EXPECT_EQ(8, prog.files_scanned); EXPECT_EQ(2, prog.dirs_found); EXPECT_EQ(2, prog.dirs_scanned); EXPECT_EQ(2, prog.symlinks_found); EXPECT_EQ(2, prog.symlinks_scanned); EXPECT_EQ(2 * with_devices + with_specials, prog.specials_found); EXPECT_EQ(file_hash_algo ? 3 + num_fail_empty : 0, prog.duplicate_files); EXPECT_EQ(1, prog.hardlinks); EXPECT_GE(prog.block_count, 1); EXPECT_GE(prog.chunk_count, 100); EXPECT_EQ(7 - prog.duplicate_files, prog.inodes_scanned); EXPECT_EQ(file_hash_algo ? 4 - num_fail_empty : 7, prog.inodes_written); EXPECT_EQ(prog.files_found - prog.duplicate_files - prog.hardlinks, prog.inodes_written); EXPECT_EQ(prog.block_count, prog.blocks_written); EXPECT_EQ(num_fail_empty, prog.errors); EXPECT_EQ(access_fail ? 2046934 : 2056934, prog.original_size); EXPECT_EQ(23456, prog.hardlink_size); EXPECT_EQ(file_hash_algo ? 23456 : 0, prog.saved_by_deduplication); EXPECT_GE(prog.saved_by_segmentation, block_size_bits == 12 ? 0 : 1000000); EXPECT_EQ(prog.original_size - (prog.saved_by_deduplication + prog.saved_by_segmentation + prog.symlink_size), prog.filesystem_size); // TODO: // EXPECT_EQ(prog.similarity_scans, similarity ? prog.inodes_scanned.load() : // 0); EXPECT_EQ(prog.similarity.bytes, similarity ? prog.original_size - (prog.saved_by_deduplication + prog.symlink_size) : 0); EXPECT_EQ(prog.hash.scans, file_hash_algo ? 5 + num_fail_empty : 0); EXPECT_EQ(prog.hash.bytes, file_hash_algo ? 46912 : 0); EXPECT_EQ(image_size, prog.compressed_size); reader::filesystem_options opts; opts.block_cache.max_bytes = 1 << 20; opts.metadata.enable_nlink = enable_nlink; opts.metadata.check_consistency = true; opts.inode_reader.readahead = readahead; reader::filesystem_v2 fs(lgr, *input, mm, opts); // fs.dump(std::cerr, 9); vfs_stat vfsbuf; fs.statvfs(&vfsbuf); EXPECT_EQ(1, vfsbuf.bsize); EXPECT_EQ(1, vfsbuf.frsize); if (enable_nlink) { EXPECT_EQ(access_fail ? 2046934 : 2056934, vfsbuf.blocks); } else { EXPECT_EQ(access_fail ? 2070390 : 2080390, vfsbuf.blocks); } EXPECT_EQ(11 + 2 * with_devices + with_specials, vfsbuf.files); EXPECT_TRUE(vfsbuf.readonly); EXPECT_GT(vfsbuf.namemax, 0); std::ostringstream dumpss; fs.dump(dumpss, {.features = reader::fsinfo_features::all()}); EXPECT_GT(dumpss.str().size(), 1000) << dumpss.str(); auto entry = fs.find("/foo.pl"); ASSERT_TRUE(entry); auto st = fs.getattr(*entry); EXPECT_EQ(st.size(), 23456); EXPECT_EQ(st.uid(), set_uid ? 0 : 1337); EXPECT_EQ(st.gid(), 0); EXPECT_EQ(st.atime(), set_time ? 4711 : keep_all_times ? 4001 : 4002); EXPECT_EQ(st.mtime(), set_time ? 4711 : keep_all_times ? 4002 : 4002); EXPECT_EQ(st.ctime(), set_time ? 4711 : keep_all_times ? 4003 : 4002); { std::error_code ec; auto st2 = fs.getattr(*entry, {.no_size = true}, ec); EXPECT_FALSE(ec); EXPECT_THROW(st2.size(), runtime_error); EXPECT_EQ(st2.uid(), st.uid()); EXPECT_EQ(st2.gid(), st.gid()); EXPECT_EQ(st2.atime(), st.atime()); EXPECT_EQ(st2.mtime(), st.mtime()); EXPECT_EQ(st2.ctime(), st.ctime()); } { auto st3 = fs.getattr(*entry, {.no_size = true}); EXPECT_THROW(st3.size(), runtime_error); EXPECT_EQ(st3.uid(), st.uid()); EXPECT_EQ(st3.gid(), st.gid()); EXPECT_EQ(st3.atime(), st.atime()); EXPECT_EQ(st3.mtime(), st.mtime()); EXPECT_EQ(st3.ctime(), st.ctime()); } int inode = fs.open(*entry); EXPECT_GE(inode, 0); std::error_code ec; std::vector buf(st.size()); auto rv = fs.read(inode, &buf[0], st.size(), ec); EXPECT_FALSE(ec); EXPECT_EQ(rv, st.size()); EXPECT_EQ(std::string(buf.begin(), buf.end()), test::loremipsum(st.size())); entry = fs.find("/somelink"); ASSERT_TRUE(entry); st = fs.getattr(*entry); EXPECT_EQ(st.size(), 16); EXPECT_EQ(st.uid(), set_uid ? 0 : 1000); EXPECT_EQ(st.gid(), set_gid ? 0 : 100); EXPECT_EQ(st.rdev(), 0); EXPECT_EQ(st.atime(), set_time ? 4711 : keep_all_times ? 2001 : 2002); EXPECT_EQ(st.mtime(), set_time ? 4711 : keep_all_times ? 2002 : 2002); EXPECT_EQ(st.ctime(), set_time ? 4711 : keep_all_times ? 2003 : 2002); auto link = fs.readlink(*entry); EXPECT_EQ(link, "somedir/ipsum.py"); EXPECT_FALSE(fs.find("/somedir/nope")); entry = fs.find("/somedir/bad"); ASSERT_TRUE(entry); st = fs.getattr(*entry); EXPECT_EQ(st.size(), 6); link = fs.readlink(*entry); EXPECT_EQ(link, "../foo"); entry = fs.find("/somedir/pipe"); if (with_specials) { ASSERT_TRUE(entry); st = fs.getattr(*entry); EXPECT_EQ(st.size(), 0); EXPECT_EQ(st.uid(), set_uid ? 0 : 1000); EXPECT_EQ(st.gid(), set_gid ? 0 : 100); EXPECT_EQ(st.type(), posix_file_type::fifo); EXPECT_EQ(st.rdev(), 0); EXPECT_EQ(st.atime(), set_time ? 4711 : keep_all_times ? 8001 : 8002); EXPECT_EQ(st.mtime(), set_time ? 4711 : keep_all_times ? 8002 : 8002); EXPECT_EQ(st.ctime(), set_time ? 4711 : keep_all_times ? 8003 : 8002); } else { EXPECT_FALSE(entry); } entry = fs.find("/somedir/null"); if (with_devices) { ASSERT_TRUE(entry); st = fs.getattr(*entry); EXPECT_EQ(st.size(), 0); EXPECT_EQ(st.uid(), 0); EXPECT_EQ(st.gid(), 0); EXPECT_EQ(st.type(), posix_file_type::character); EXPECT_EQ(st.rdev(), 259); } else { EXPECT_FALSE(entry); } entry = fs.find("/somedir/zero"); if (with_devices) { ASSERT_TRUE(entry); st = fs.getattr(*entry); EXPECT_EQ(st.size(), 0); EXPECT_EQ(st.uid(), 0); EXPECT_EQ(st.gid(), 0); EXPECT_EQ(st.type(), posix_file_type::character); EXPECT_EQ(st.rdev(), 261); EXPECT_EQ(st.atime(), set_time ? 4711 : keep_all_times ? 4000010001 : 4000020002); EXPECT_EQ(st.mtime(), set_time ? 4711 : keep_all_times ? 4000020002 : 4000020002); EXPECT_EQ(st.ctime(), set_time ? 4711 : keep_all_times ? 4000030003 : 4000020002); } else { EXPECT_FALSE(entry); } entry = fs.find("/"); ASSERT_TRUE(entry); auto dir = fs.opendir(*entry); ASSERT_TRUE(dir); EXPECT_EQ(10, fs.dirsize(*dir)); entry = fs.find("/somedir"); ASSERT_TRUE(entry); dir = fs.opendir(*entry); ASSERT_TRUE(dir); EXPECT_EQ(5 + 2 * with_devices + with_specials, fs.dirsize(*dir)); std::vector names; for (size_t i = 0; i < fs.dirsize(*dir); ++i) { auto r = fs.readdir(*dir, i); ASSERT_TRUE(r); auto [view, name] = *r; names.emplace_back(name); } std::vector expected{ ".", "..", "bad", "empty", "ipsum.py", }; if (with_devices) { expected.emplace_back("null"); } if (with_specials) { expected.emplace_back("pipe"); } if (with_devices) { expected.emplace_back("zero"); } EXPECT_EQ(expected, names); entry = fs.find("/foo.pl"); ASSERT_TRUE(entry); auto e2 = fs.find("/bar.pl"); ASSERT_TRUE(e2); EXPECT_EQ(entry->inode_num(), e2->inode_num()); auto st1 = fs.getattr(*entry); auto st2 = fs.getattr(*e2); EXPECT_EQ(st1.ino(), st2.ino()); if (enable_nlink) { EXPECT_EQ(2, st1.nlink()); EXPECT_EQ(2, st2.nlink()); } entry = fs.find("/"); ASSERT_TRUE(entry); EXPECT_EQ(0, entry->inode_num()); e2 = fs.find(0); ASSERT_TRUE(e2); EXPECT_EQ(e2->inode_num(), 0); entry = fs.find(0, "baz.pl"); ASSERT_TRUE(entry); EXPECT_GT(entry->inode_num(), 0); st1 = fs.getattr(*entry); EXPECT_EQ(23456, st1.size()); e2 = fs.find(0, "somedir"); ASSERT_TRUE(e2); st2 = fs.getattr(*e2); entry = fs.find(st2.ino(), "ipsum.py"); ASSERT_TRUE(entry); st1 = fs.getattr(*entry); EXPECT_EQ(access_fail ? 0 : 10000, st1.size()); EXPECT_TRUE(fs.access(*entry, R_OK, 1000, 100)); entry = fs.find(0, "baz.pl"); ASSERT_TRUE(entry); fs.access(*entry, R_OK, 1337, 0, ec); EXPECT_EQ(set_uid ? EACCES : 0, ec.value()); EXPECT_EQ(set_uid, !fs.access(*entry, R_OK, 1337, 0)); for (auto mp : {&reader::filesystem_v2::walk, &reader::filesystem_v2::walk_data_order}) { std::map entries; std::vector inodes; (fs.*mp)([&](reader::dir_entry_view e) { auto stbuf = fs.getattr(e.inode()); inodes.push_back(stbuf.ino()); auto path = e.path(); if (!path.empty()) { path = "/" + path; } EXPECT_TRUE(entries.emplace(path, stbuf).second); }); EXPECT_EQ(entries.size(), input->size() + 2 * with_devices + with_specials - 3); for (auto const& [p, st] : entries) { auto ref = input->symlink_info(p); EXPECT_EQ(ref.mode(), st.mode()) << p; EXPECT_EQ(set_uid ? 0 : ref.uid(), st.uid()) << p; EXPECT_EQ(set_gid ? 0 : ref.gid(), st.gid()) << p; if (!st.is_directory()) { if (input->access(p, R_OK) == 0) { EXPECT_EQ(ref.size(), st.size()) << p; } else { EXPECT_EQ(0, st.size()) << p; } } } } auto dyn = fs.metadata_as_json(); EXPECT_TRUE(dyn.is_object()); auto json = fs.serialize_metadata_as_json(true); EXPECT_GT(json.size(), 1000) << json; json = fs.serialize_metadata_as_json(false); EXPECT_GT(json.size(), 1000) << json; for (int detail = 0; detail <= 5; ++detail) { auto info = fs.info_as_json( {.features = reader::fsinfo_features::for_level(detail)}); ASSERT_TRUE(info.count("version")); ASSERT_TRUE(info.count("image_offset")); ASSERT_TRUE(info.count("created_on")); ASSERT_TRUE(info.count("created_by")); if (detail >= 1) { ASSERT_TRUE(info.count("block_count")); ASSERT_TRUE(info.count("block_size")); ASSERT_TRUE(info.count("compressed_block_size")); ASSERT_TRUE(info.count("compressed_metadata_size")); ASSERT_TRUE(info.count("inode_count")); ASSERT_TRUE(info.count("options")); ASSERT_TRUE(info.count("original_filesystem_size")); ASSERT_TRUE(info.count("preferred_path_separator")); ASSERT_TRUE(info.count("uncompressed_block_size")); ASSERT_TRUE(info.count("uncompressed_metadata_size")); } if (detail >= 2) { ASSERT_TRUE(info.count("history")); } if (detail >= 3) { ASSERT_TRUE(info.count("meta")); ASSERT_TRUE(info.count("sections")); } if (detail >= 4) { ASSERT_TRUE(info.count("root")); } } } std::vector const compressions{ "null", #ifdef DWARFS_HAVE_LIBLZ4 "lz4", "lz4hc:level=4", #endif #ifdef DWARFS_HAVE_LIBZSTD "zstd:level=1", #endif #ifdef DWARFS_HAVE_LIBLZMA "lzma:level=1", "lzma:level=1:binary=x86", #endif #ifdef DWARFS_HAVE_LIBBROTLI "brotli:quality=2", #endif }; } // namespace class compression_test : public testing::TestWithParam< std::tuple>> { DWARFS_SLOW_FIXTURE }; class scanner_test : public testing::TestWithParam< std::tuple>> {}; class hashing_test : public testing::TestWithParam {}; class packing_test : public testing::TestWithParam< std::tuple> { }; class plain_tables_test : public testing::TestWithParam> {}; TEST_P(compression_test, end_to_end) { auto [compressor, block_size_bits, file_order, file_hash_algo] = GetParam(); if (compressor.find("lzma") == 0 && block_size_bits < 16) { // these are notoriously slow, so just skip them return; } size_t readahead = 0; if (block_size_bits < 20) { readahead = static_cast(4) << block_size_bits; } basic_end_to_end_test(compressor, block_size_bits, file_order, true, true, false, false, false, false, false, true, true, true, true, true, true, true, false, false, false, readahead, file_hash_algo); } TEST_P(scanner_test, end_to_end) { auto [with_devices, with_specials, set_uid, set_gid, set_time, keep_all_times, enable_nlink, access_fail, file_hash_algo] = GetParam(); basic_end_to_end_test(compressions[0], 15, writer::fragment_order_mode::NONE, with_devices, with_specials, set_uid, set_gid, set_time, keep_all_times, enable_nlink, true, true, true, true, true, true, true, false, false, access_fail, 0, file_hash_algo); } TEST_P(hashing_test, end_to_end) { basic_end_to_end_test(compressions[0], 15, writer::fragment_order_mode::NONE, true, true, true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, 0, GetParam()); } TEST_P(packing_test, end_to_end) { auto [pack_chunk_table, pack_directories, pack_shared_files_table, pack_names, pack_names_index, pack_symlinks, pack_symlinks_index] = GetParam(); basic_end_to_end_test( compressions[0], 15, writer::fragment_order_mode::NONE, true, true, false, false, false, false, false, pack_chunk_table, pack_directories, pack_shared_files_table, pack_names, pack_names_index, pack_symlinks, pack_symlinks_index, false, false, false, 0, default_file_hash_algo); } TEST_P(plain_tables_test, end_to_end) { auto [plain_names_table, plain_symlinks_table] = GetParam(); basic_end_to_end_test(compressions[0], 15, writer::fragment_order_mode::NONE, true, true, false, false, false, false, false, false, false, false, false, false, false, false, plain_names_table, plain_symlinks_table, false, 0, default_file_hash_algo); } TEST_P(packing_test, regression_empty_fs) { auto [pack_chunk_table, pack_directories, pack_shared_files_table, pack_names, pack_names_index, pack_symlinks, pack_symlinks_index] = GetParam(); writer::segmenter::config cfg; writer::scanner_options options; cfg.blockhash_window_size = 8; cfg.block_size_bits = 10; options.pack_chunk_table = pack_chunk_table; options.pack_directories = pack_directories; options.pack_shared_files_table = pack_shared_files_table; options.pack_names = pack_names; options.pack_names_index = pack_names_index; options.pack_symlinks = pack_symlinks; options.pack_symlinks_index = pack_symlinks_index; options.force_pack_string_tables = true; test::test_logger lgr; auto input = std::make_shared(); input->add_dir(""); auto mm = std::make_shared( build_dwarfs(lgr, input, "null", cfg, options)); reader::filesystem_options opts; opts.block_cache.max_bytes = 1 << 20; opts.metadata.check_consistency = true; reader::filesystem_v2 fs(lgr, *input, mm, opts); vfs_stat vfsbuf; fs.statvfs(&vfsbuf); EXPECT_EQ(1, vfsbuf.files); EXPECT_EQ(0, vfsbuf.blocks); size_t num = 0; fs.walk([&](reader::dir_entry_view e) { ++num; auto stbuf = fs.getattr(e.inode()); EXPECT_TRUE(stbuf.is_directory()); }); EXPECT_EQ(1, num); } INSTANTIATE_TEST_SUITE_P( dwarfs, compression_test, ::testing::Combine( ::testing::ValuesIn(compressions), ::testing::Values(12, 15, 20, 28), ::testing::Values(writer::fragment_order_mode::NONE, writer::fragment_order_mode::PATH, writer::fragment_order_mode::REVPATH, writer::fragment_order_mode::NILSIMSA, writer::fragment_order_mode::SIMILARITY), ::testing::Values(std::nullopt, "xxh3-128"))); INSTANTIATE_TEST_SUITE_P( dwarfs, scanner_test, ::testing::Combine(::testing::Bool(), ::testing::Bool(), ::testing::Bool(), ::testing::Bool(), ::testing::Bool(), ::testing::Bool(), ::testing::Bool(), ::testing::Bool(), ::testing::Values(std::nullopt, "xxh3-128", "sha512"))); INSTANTIATE_TEST_SUITE_P(dwarfs, hashing_test, ::testing::ValuesIn(checksum::available_algorithms())); INSTANTIATE_TEST_SUITE_P( dwarfs, packing_test, ::testing::Combine(::testing::Bool(), ::testing::Bool(), ::testing::Bool(), ::testing::Bool(), ::testing::Bool(), ::testing::Bool(), ::testing::Bool())); INSTANTIATE_TEST_SUITE_P(dwarfs, plain_tables_test, ::testing::Combine(::testing::Bool(), ::testing::Bool())); TEST(segmenter, regression_block_boundary) { writer::segmenter::config cfg; // make sure we don't actually segment anything cfg.blockhash_window_size = 12; cfg.block_size_bits = 10; reader::filesystem_options opts; opts.block_cache.max_bytes = 1 << 20; opts.metadata.check_consistency = true; test::test_logger lgr; std::vector fs_blocks; for (auto size : {1023, 1024, 1025}) { auto input = std::make_shared(); input->add_dir(""); input->add_file("test", size); auto fsdata = build_dwarfs(lgr, input, "null", cfg); auto mm = std::make_shared(fsdata); reader::filesystem_v2 fs(lgr, *input, mm, opts); vfs_stat vfsbuf; fs.statvfs(&vfsbuf); EXPECT_EQ(2, vfsbuf.files); EXPECT_EQ(size, vfsbuf.blocks); fs_blocks.push_back(fs.num_blocks()); } std::vector const fs_blocks_expected{1, 1, 2}; EXPECT_EQ(fs_blocks_expected, fs_blocks); } class compression_regression : public testing::TestWithParam {}; TEST_P(compression_regression, github45) { auto compressor = GetParam(); writer::segmenter::config cfg; constexpr size_t block_size_bits = 18; constexpr size_t file_size = 1 << block_size_bits; cfg.blockhash_window_size = 0; cfg.block_size_bits = block_size_bits; reader::filesystem_options opts; opts.block_cache.max_bytes = 1 << 20; opts.metadata.check_consistency = true; test::test_logger lgr; std::independent_bits_engine::digits, uint16_t> rng; std::string random; random.resize(file_size); std::generate(begin(random), end(random), std::ref(rng)); auto input = std::make_shared(); input->add_dir(""); input->add_file("random", random); input->add_file("test", file_size); auto fsdata = build_dwarfs(lgr, input, compressor, cfg); auto mm = std::make_shared(fsdata); std::stringstream idss; reader::filesystem_v2::identify(lgr, *input, mm, idss, 3); std::string line; std::regex const re("^SECTION num=\\d+, type=BLOCK, compression=(\\w+).*"); std::set compressions; while (std::getline(idss, line)) { std::smatch m; if (std::regex_match(line, m, re)) { compressions.emplace(m[1]); } } if (compressor == "null") { EXPECT_EQ(1, compressions.size()); } else { EXPECT_EQ(2, compressions.size()); } EXPECT_EQ(1, compressions.count("NONE")); reader::filesystem_v2 fs(lgr, *input, mm, opts); vfs_stat vfsbuf; fs.statvfs(&vfsbuf); EXPECT_EQ(3, vfsbuf.files); EXPECT_EQ(2 * file_size, vfsbuf.blocks); auto check_file = [&](char const* name, std::string const& contents) { auto entry = fs.find(name); ASSERT_TRUE(entry); auto st = fs.getattr(*entry); EXPECT_EQ(st.size(), file_size); int inode = fs.open(*entry); EXPECT_GE(inode, 0); auto buf = fs.read_string(inode); EXPECT_EQ(buf, contents); }; check_file("random", random); check_file("test", test::loremipsum(file_size)); } INSTANTIATE_TEST_SUITE_P(dwarfs, compression_regression, ::testing::ValuesIn(compressions)); class file_scanner : public testing::TestWithParam< std::tuple>> { DWARFS_SLOW_FIXTURE }; TEST_P(file_scanner, inode_ordering) { auto [order_mode, file_hash_algo] = GetParam(); test::test_logger lgr; auto bmcfg = writer::segmenter::config(); auto opts = writer::scanner_options(); writer::fragment_order_options order_opts; order_opts.mode = order_mode; opts.file_hash_algorithm = file_hash_algo; opts.inode.fragment_order.set_default(order_opts); opts.no_create_timestamp = true; auto input = std::make_shared(); #if defined(DWARFS_TEST_RUNNING_ON_ASAN) || defined(DWARFS_TEST_RUNNING_ON_TSAN) static constexpr int dim{7}; #else static constexpr int dim{14}; #endif #ifdef NDEBUG static constexpr int repetitions{50}; #else static constexpr int repetitions{10}; #endif input->add_dir(""); for (int x = 0; x < dim; ++x) { input->add_dir(fmt::format("{}", x)); for (int y = 0; y < dim; ++y) { input->add_dir(fmt::format("{}/{}", x, y)); for (int z = 0; z < dim; ++z) { input->add_file(fmt::format("{}/{}/{}", x, y, z), (x + 1) * (y + 1) * (z + 1), true); } } } auto ref = build_dwarfs(lgr, input, "null", bmcfg, opts); for (int i = 0; i < repetitions; ++i) { auto fs = build_dwarfs(lgr, input, "null", bmcfg, opts); EXPECT_EQ(ref, fs); // if (ref != fs) { // folly::writeFile(ref, "ref.dwarfs"); // folly::writeFile(fs, fmt::format("test{}.dwarfs", i).c_str()); // } } } INSTANTIATE_TEST_SUITE_P( dwarfs, file_scanner, ::testing::Combine( ::testing::Values(writer::fragment_order_mode::PATH, writer::fragment_order_mode::REVPATH, writer::fragment_order_mode::SIMILARITY, writer::fragment_order_mode::NILSIMSA), ::testing::Values(std::nullopt, "xxh3-128"))); class filter_test : public testing::TestWithParam { public: test::test_logger lgr; std::unique_ptr rbf; std::shared_ptr tfa; std::shared_ptr input; void SetUp() override { tfa = std::make_shared(); rbf = std::make_unique(lgr, tfa); rbf->set_root_path(""); input = std::make_shared(); for (auto const& [stat, name] : dwarfs::test::test_dirtree()) { auto path = name.substr(name.size() == 5 ? 5 : 6); switch (stat.type()) { case posix_file_type::regular: input->add(path, stat, [size = stat.size] { return test::loremipsum(size); }); break; case posix_file_type::symlink: input->add(path, stat, test::loremipsum(stat.size)); break; default: input->add(path, stat); break; } } } void set_filter_rules(test::filter_test_data const& spec) { std::istringstream iss(spec.filter()); rbf->add_rules(iss); } std::string get_filter_debug_output(test::filter_test_data const& spec, writer::debug_filter_mode mode) { set_filter_rules(spec); std::ostringstream oss; writer::scanner_options options; options.remove_empty_dirs = false; options.debug_filter_function = [&](bool exclude, writer::entry_interface const& ei) { debug_filter_output(oss, exclude, ei, mode); }; writer::writer_progress prog; thread_pool pool(lgr, *input, "worker", 1); writer::segmenter_factory sf(lgr, prog); writer::entry_factory ef; writer::scanner s(lgr, pool, sf, ef, *input, options); s.add_filter(std::move(rbf)); block_compressor bc("null"); std::ostringstream null; writer::filesystem_writer fsw(null, lgr, pool, prog); fsw.add_default_compressor(bc); s.scan(fsw, std::filesystem::path("/"), prog); return oss.str(); } void TearDown() override { rbf.reset(); input.reset(); tfa.reset(); } }; TEST_P(filter_test, filesystem) { auto spec = GetParam(); set_filter_rules(spec); writer::segmenter::config cfg; writer::scanner_options options; options.remove_empty_dirs = true; auto fsimage = build_dwarfs(lgr, input, "null", cfg, options, nullptr, nullptr, std::nullopt, std::move(rbf)); auto mm = std::make_shared(std::move(fsimage)); reader::filesystem_options opts; opts.block_cache.max_bytes = 1 << 20; opts.metadata.enable_nlink = true; opts.metadata.check_consistency = true; reader::filesystem_v2 fs(lgr, *input, mm, opts); std::unordered_set got; fs.walk([&got](reader::dir_entry_view e) { got.emplace(e.unix_path()); }); EXPECT_EQ(spec.expected_files(), got); } TEST_P(filter_test, debug_filter_function_included) { auto spec = GetParam(); auto output = get_filter_debug_output(spec, writer::debug_filter_mode::INCLUDED); auto expected = spec.get_expected_filter_output(writer::debug_filter_mode::INCLUDED); EXPECT_EQ(expected, output); } TEST_P(filter_test, debug_filter_function_included_files) { auto spec = GetParam(); auto output = get_filter_debug_output(spec, writer::debug_filter_mode::INCLUDED_FILES); auto expected = spec.get_expected_filter_output( writer::debug_filter_mode::INCLUDED_FILES); EXPECT_EQ(expected, output); } TEST_P(filter_test, debug_filter_function_excluded) { auto spec = GetParam(); auto output = get_filter_debug_output(spec, writer::debug_filter_mode::EXCLUDED); auto expected = spec.get_expected_filter_output(writer::debug_filter_mode::EXCLUDED); EXPECT_EQ(expected, output); } TEST_P(filter_test, debug_filter_function_excluded_files) { auto spec = GetParam(); auto output = get_filter_debug_output(spec, writer::debug_filter_mode::EXCLUDED_FILES); auto expected = spec.get_expected_filter_output( writer::debug_filter_mode::EXCLUDED_FILES); EXPECT_EQ(expected, output); } TEST_P(filter_test, debug_filter_function_all) { auto spec = GetParam(); auto output = get_filter_debug_output(spec, writer::debug_filter_mode::ALL); auto expected = spec.get_expected_filter_output(writer::debug_filter_mode::ALL); EXPECT_EQ(expected, output); } TEST_P(filter_test, debug_filter_function_files) { auto spec = GetParam(); auto output = get_filter_debug_output(spec, writer::debug_filter_mode::FILES); auto expected = spec.get_expected_filter_output(writer::debug_filter_mode::FILES); EXPECT_EQ(expected, output); } INSTANTIATE_TEST_SUITE_P(dwarfs, filter_test, ::testing::ValuesIn(dwarfs::test::get_filter_tests())); TEST(file_scanner, input_list) { test::test_logger lgr; auto bmcfg = writer::segmenter::config(); auto opts = writer::scanner_options(); writer::fragment_order_options order_opts; opts.inode.fragment_order.set_default(order_opts); auto input = test::os_access_mock::create_test_instance(); std::vector input_list{ "somedir/ipsum.py", "foo.pl", }; auto fsimage = build_dwarfs(lgr, input, "null", bmcfg, opts, nullptr, nullptr, input_list); auto mm = std::make_shared(std::move(fsimage)); reader::filesystem_v2 fs(lgr, *input, mm); std::unordered_set got; fs.walk([&got](reader::dir_entry_view e) { got.emplace(e.unix_path()); }); std::unordered_set expected{ "", "somedir", "somedir/ipsum.py", "foo.pl", }; EXPECT_EQ(expected, got); } TEST(filesystem, uid_gid_32bit) { test::test_logger lgr; auto input = std::make_shared(); input->add("", {1, 040755, 1, 0, 0, 10, 42, 0, 0, 0}); input->add("foo16.txt", {2, 0100755, 1, 60000, 65535, 5, 42, 0, 0, 0}, "hello"); input->add("foo32.txt", {3, 0100755, 1, 65536, 4294967295, 5, 42, 0, 0, 0}, "world"); auto fsimage = build_dwarfs(lgr, input, "null"); auto mm = std::make_shared(std::move(fsimage)); reader::filesystem_v2 fs(lgr, *input, mm); auto iv16 = fs.find("/foo16.txt"); auto iv32 = fs.find("/foo32.txt"); EXPECT_TRUE(iv16); EXPECT_TRUE(iv32); auto st16 = fs.getattr(*iv16); auto st32 = fs.getattr(*iv32); EXPECT_EQ(60000, st16.uid()); EXPECT_EQ(65535, st16.gid()); EXPECT_EQ(65536, st32.uid()); EXPECT_EQ(4294967295, st32.gid()); } TEST(filesystem, uid_gid_count) { test::test_logger lgr; auto input = std::make_shared(); input->add("", {1, 040755, 1, 0, 0, 10, 42, 0, 0, 0}); for (uint32_t i = 0; i < 100000; ++i) { input->add(fmt::format("foo{:05d}.txt", i), {2 + i, 0100644, 1, 50000 + i, 250000 + i, 10, 42, 0, 0, 0}, fmt::format("hello{:05d}", i)); } auto fsimage = build_dwarfs(lgr, input, "null"); auto mm = std::make_shared(std::move(fsimage)); reader::filesystem_v2 fs(lgr, *input, mm); auto iv00000 = fs.find("/foo00000.txt"); auto iv50000 = fs.find("/foo50000.txt"); auto iv99999 = fs.find("/foo99999.txt"); EXPECT_TRUE(iv00000); EXPECT_TRUE(iv50000); EXPECT_TRUE(iv99999); auto st00000 = fs.getattr(*iv00000); auto st50000 = fs.getattr(*iv50000); auto st99999 = fs.getattr(*iv99999); EXPECT_EQ(50000, st00000.uid()); EXPECT_EQ(250000, st00000.gid()); EXPECT_EQ(100000, st50000.uid()); EXPECT_EQ(300000, st50000.gid()); EXPECT_EQ(149999, st99999.uid()); EXPECT_EQ(349999, st99999.gid()); } TEST(section_index_regression, github183) { static constexpr uint64_t section_offset_mask{(UINT64_C(1) << 48) - 1}; test::test_logger lgr; writer::segmenter::config cfg{ .block_size_bits = 10, }; auto input = test::os_access_mock::create_test_instance(); auto fsimage = build_dwarfs(lgr, input, "null", cfg); std::vector index; { uint64_t index_pos; ::memcpy(&index_pos, fsimage.data() + (fsimage.size() - sizeof(uint64_t)), sizeof(uint64_t)); ASSERT_EQ((index_pos >> 48), static_cast(section_type::SECTION_INDEX)); index_pos &= section_offset_mask; ASSERT_LT(index_pos, fsimage.size()); test::mmap_mock mm(fsimage); auto section = internal::fs_section(mm, index_pos, 2); EXPECT_TRUE(section.check_fast(mm)); index.resize(section.length() / sizeof(uint64_t)); ::memcpy(index.data(), section.data(mm).data(), section.length()); } ASSERT_GT(index.size(), 10); auto const schema_ix{index.size() - 4}; auto const metadata_ix{index.size() - 3}; auto const history_ix{index.size() - 2}; ASSERT_EQ(index[schema_ix] >> 48, static_cast(section_type::METADATA_V2_SCHEMA)); ASSERT_EQ(index[metadata_ix] >> 48, static_cast(section_type::METADATA_V2)); ASSERT_EQ(index[history_ix] >> 48, static_cast(section_type::HISTORY)); auto const schema_offset{index[schema_ix] & section_offset_mask}; auto fsimage2 = fsimage; ::memset(fsimage2.data() + 8, 0xff, schema_offset - 8); auto mm = std::make_shared(fsimage2); reader::filesystem_v2 fs; ASSERT_NO_THROW(fs = reader::filesystem_v2(lgr, *input, mm)); EXPECT_NO_THROW(fs.walk([](auto) {})); auto entry = fs.find("/foo.pl"); ASSERT_TRUE(entry); auto st = fs.getattr(*entry); int inode{-1}; EXPECT_NO_THROW(inode = fs.open(*entry)); std::error_code ec; std::vector buf(st.size()); auto rv = fs.read(inode, &buf[0], st.size(), ec); EXPECT_TRUE(ec); EXPECT_EQ(rv, 0); EXPECT_EQ(ec.value(), EIO); std::stringstream idss; EXPECT_THROW(reader::filesystem_v2::identify(lgr, *input, mm, idss, 3), dwarfs::runtime_error); } TEST(filesystem, find_by_path) { test::test_logger lgr; auto input = test::os_access_mock::create_test_instance(); auto fsimage = build_dwarfs(lgr, input, "null"); auto mm = std::make_shared(std::move(fsimage)); reader::filesystem_v2 fs(lgr, *input, mm); std::vector paths; fs.walk([&](auto e) { paths.emplace_back(e.unix_path()); }); EXPECT_GT(paths.size(), 10); for (auto const& p : paths) { auto iv = fs.find(p.c_str()); ASSERT_TRUE(iv) << p; EXPECT_FALSE(fs.find(iv->inode_num(), "desktop.ini")) << p; EXPECT_FALSE(fs.find((p + "/desktop.ini").c_str())) << p; } } TEST(file_scanner, file_start_hash) { test::test_logger lgr; auto input = std::make_shared(); static constexpr size_t const kSize{1 << 20}; auto generator = [] { return test::loremipsum(kSize); }; input->add("", {1, 040755, 1, 0, 0, 10, 42, 0, 0, 0}); input->add("hardlink1", {42, 0100755, 2, 1000, 100, kSize, 4711, 0, 0, 0}, generator); input->add("hardlink2", {42, 0100755, 2, 1000, 100, kSize, 4711, 0, 0, 0}, generator); auto fsimage = build_dwarfs(lgr, input, "null"); auto mm = std::make_shared(std::move(fsimage)); reader::filesystem_v2 fs(lgr, *input, mm, {.metadata = {.enable_nlink = true}}); auto link1 = fs.find("/hardlink1"); auto link2 = fs.find("/hardlink2"); EXPECT_TRUE(link1); EXPECT_TRUE(link2); auto st1 = fs.getattr(*link1); auto st2 = fs.getattr(*link2); EXPECT_EQ(st1.ino(), st2.ino()); EXPECT_EQ(st1.nlink(), 2); EXPECT_EQ(st2.nlink(), 2); } TEST(filesystem, root_access_github204) { test::test_logger lgr; auto input = std::make_shared(); input->add("", {1, 040755, 1, 1000, 100, 10, 42, 0, 0, 0}); input->add("other", {2, 040755, 1, 1000, 100, 10, 42, 0, 0, 0}); input->add("group", {3, 040750, 1, 1000, 100, 10, 42, 0, 0, 0}); input->add("user", {4, 040700, 1, 1000, 100, 10, 42, 0, 0, 0}); input->add("other/file", {5, 0100644, 1, 1000, 100, 5, 42, 0, 0, 0}, "other"); input->add("group/file", {6, 0100640, 1, 1000, 100, 5, 42, 0, 0, 0}, "group"); input->add("user/file", {7, 0100600, 1, 1000, 100, 4, 42, 0, 0, 0}, "user"); auto fsimage = build_dwarfs(lgr, input, "null"); auto mm = std::make_shared(std::move(fsimage)); reader::filesystem_v2 fs(lgr, *input, mm); auto other = fs.find("/other"); auto group = fs.find("/group"); auto user = fs.find("/user"); EXPECT_TRUE(other); EXPECT_TRUE(group); EXPECT_TRUE(user); #ifdef _WIN32 static constexpr int const x_ok{1}; #else static constexpr int const x_ok{X_OK}; #endif EXPECT_TRUE(fs.access(*other, R_OK, 1000, 100)); EXPECT_TRUE(fs.access(*group, R_OK, 1000, 100)); EXPECT_TRUE(fs.access(*user, R_OK, 1000, 100)); EXPECT_TRUE(fs.access(*other, W_OK, 1000, 100)); EXPECT_TRUE(fs.access(*group, W_OK, 1000, 100)); EXPECT_TRUE(fs.access(*user, W_OK, 1000, 100)); EXPECT_TRUE(fs.access(*other, x_ok, 1000, 100)); EXPECT_TRUE(fs.access(*group, x_ok, 1000, 100)); EXPECT_TRUE(fs.access(*user, x_ok, 1000, 100)); EXPECT_TRUE(fs.access(*other, R_OK, 1000, 0)); EXPECT_TRUE(fs.access(*group, R_OK, 1000, 0)); EXPECT_TRUE(fs.access(*user, R_OK, 1000, 0)); EXPECT_TRUE(fs.access(*other, W_OK, 1000, 0)); EXPECT_TRUE(fs.access(*group, W_OK, 1000, 0)); EXPECT_TRUE(fs.access(*user, W_OK, 1000, 0)); EXPECT_TRUE(fs.access(*other, x_ok, 1000, 0)); EXPECT_TRUE(fs.access(*group, x_ok, 1000, 0)); EXPECT_TRUE(fs.access(*user, x_ok, 1000, 0)); EXPECT_TRUE(fs.access(*other, R_OK, 2000, 100)); EXPECT_TRUE(fs.access(*group, R_OK, 2000, 100)); EXPECT_FALSE(fs.access(*user, R_OK, 2000, 100)); EXPECT_FALSE(fs.access(*other, W_OK, 2000, 100)); EXPECT_FALSE(fs.access(*group, W_OK, 2000, 100)); EXPECT_FALSE(fs.access(*user, W_OK, 2000, 100)); EXPECT_TRUE(fs.access(*other, x_ok, 2000, 100)); EXPECT_TRUE(fs.access(*group, x_ok, 2000, 100)); EXPECT_FALSE(fs.access(*user, x_ok, 2000, 100)); EXPECT_TRUE(fs.access(*other, R_OK, 2000, 200)); EXPECT_FALSE(fs.access(*group, R_OK, 2000, 200)); EXPECT_FALSE(fs.access(*user, R_OK, 2000, 200)); EXPECT_FALSE(fs.access(*other, W_OK, 2000, 200)); EXPECT_FALSE(fs.access(*group, W_OK, 2000, 200)); EXPECT_FALSE(fs.access(*user, W_OK, 2000, 200)); EXPECT_TRUE(fs.access(*other, x_ok, 2000, 200)); EXPECT_FALSE(fs.access(*group, x_ok, 2000, 200)); EXPECT_FALSE(fs.access(*user, x_ok, 2000, 200)); EXPECT_TRUE(fs.access(*other, R_OK, 0, 0)); EXPECT_TRUE(fs.access(*group, R_OK, 0, 0)); EXPECT_TRUE(fs.access(*user, R_OK, 0, 0)); EXPECT_TRUE(fs.access(*other, W_OK, 0, 0)); EXPECT_TRUE(fs.access(*group, W_OK, 0, 0)); EXPECT_TRUE(fs.access(*user, W_OK, 0, 0)); EXPECT_TRUE(fs.access(*other, x_ok, 0, 0)); EXPECT_TRUE(fs.access(*group, x_ok, 0, 0)); EXPECT_TRUE(fs.access(*user, x_ok, 0, 0)); other = fs.find("/other/file"); group = fs.find("/group/file"); user = fs.find("/user/file"); EXPECT_TRUE(other); EXPECT_TRUE(group); EXPECT_TRUE(user); EXPECT_TRUE(fs.access(*other, R_OK, 1000, 100)); EXPECT_TRUE(fs.access(*group, R_OK, 1000, 100)); EXPECT_TRUE(fs.access(*user, R_OK, 1000, 100)); EXPECT_TRUE(fs.access(*other, W_OK, 1000, 100)); EXPECT_TRUE(fs.access(*group, W_OK, 1000, 100)); EXPECT_TRUE(fs.access(*user, W_OK, 1000, 100)); EXPECT_FALSE(fs.access(*other, x_ok, 1000, 100)); EXPECT_FALSE(fs.access(*group, x_ok, 1000, 100)); EXPECT_FALSE(fs.access(*user, x_ok, 1000, 100)); EXPECT_TRUE(fs.access(*other, R_OK, 1000, 0)); EXPECT_TRUE(fs.access(*group, R_OK, 1000, 0)); EXPECT_TRUE(fs.access(*user, R_OK, 1000, 0)); EXPECT_TRUE(fs.access(*other, W_OK, 1000, 0)); EXPECT_TRUE(fs.access(*group, W_OK, 1000, 0)); EXPECT_TRUE(fs.access(*user, W_OK, 1000, 0)); EXPECT_FALSE(fs.access(*other, x_ok, 1000, 0)); EXPECT_FALSE(fs.access(*group, x_ok, 1000, 0)); EXPECT_FALSE(fs.access(*user, x_ok, 1000, 0)); EXPECT_TRUE(fs.access(*other, R_OK, 2000, 100)); EXPECT_TRUE(fs.access(*group, R_OK, 2000, 100)); EXPECT_FALSE(fs.access(*user, R_OK, 2000, 100)); EXPECT_FALSE(fs.access(*other, W_OK, 2000, 100)); EXPECT_FALSE(fs.access(*group, W_OK, 2000, 100)); EXPECT_FALSE(fs.access(*user, W_OK, 2000, 100)); EXPECT_FALSE(fs.access(*other, x_ok, 2000, 100)); EXPECT_FALSE(fs.access(*group, x_ok, 2000, 100)); EXPECT_FALSE(fs.access(*user, x_ok, 2000, 100)); EXPECT_TRUE(fs.access(*other, R_OK, 2000, 200)); EXPECT_FALSE(fs.access(*group, R_OK, 2000, 200)); EXPECT_FALSE(fs.access(*user, R_OK, 2000, 200)); EXPECT_FALSE(fs.access(*other, W_OK, 2000, 200)); EXPECT_FALSE(fs.access(*group, W_OK, 2000, 200)); EXPECT_FALSE(fs.access(*user, W_OK, 2000, 200)); EXPECT_FALSE(fs.access(*other, x_ok, 2000, 200)); EXPECT_FALSE(fs.access(*group, x_ok, 2000, 200)); EXPECT_FALSE(fs.access(*user, x_ok, 2000, 200)); EXPECT_TRUE(fs.access(*other, R_OK, 0, 0)); EXPECT_TRUE(fs.access(*group, R_OK, 0, 0)); EXPECT_TRUE(fs.access(*user, R_OK, 0, 0)); EXPECT_TRUE(fs.access(*other, W_OK, 0, 0)); EXPECT_TRUE(fs.access(*group, W_OK, 0, 0)); EXPECT_TRUE(fs.access(*user, W_OK, 0, 0)); EXPECT_FALSE(fs.access(*other, x_ok, 0, 0)); EXPECT_FALSE(fs.access(*group, x_ok, 0, 0)); EXPECT_FALSE(fs.access(*user, x_ok, 0, 0)); } TEST(filesystem, read) { test::test_logger lgr; std::independent_bits_engine::digits, uint16_t> rng; std::string contents; contents.resize(76543); std::generate(begin(contents), end(contents), std::ref(rng)); auto input = std::make_shared(); input->add_dir(""); input->add_file("random", contents); auto fsimage = build_dwarfs(lgr, input, "null", {.block_size_bits = 8}); auto mm = std::make_shared(std::move(fsimage)); reader::filesystem_v2 fs(lgr, *input, mm, {.inode_reader = {.readahead = 64}}); auto iv = fs.find("/random"); EXPECT_TRUE(iv); auto fh = fs.open(*iv); uint32_t fh_invalid = 66666; std::error_code ec; std::vector tmp(contents.size()); reader::iovec_read_buf iov; std::string_view cview(contents); auto iov_to_str = [](reader::iovec_read_buf const& iov) { std::string result; for (auto const& i : iov.buf) { result.append(reinterpret_cast(i.iov_base), i.iov_len); } return result; }; auto fut_to_str = [](std::vector>&& futs) { std::string result; for (auto& f : futs) { auto br = f.get(); result.append(reinterpret_cast(br.data()), br.size()); } return result; }; // --- read_string --- EXPECT_EQ(fs.read_string(fh), cview); EXPECT_EQ(fs.read_string(fh, ec), cview); EXPECT_FALSE(ec); EXPECT_THROW(fs.read_string(fh_invalid), std::system_error); fs.read_string(fh_invalid, ec); EXPECT_TRUE(ec); EXPECT_EQ(ec.value(), EINVAL); // --- read --- std::fill(begin(tmp), end(tmp), 0); EXPECT_EQ(fs.read(fh, tmp.data(), tmp.size()), cview.size()); EXPECT_EQ(std::string_view(tmp.data(), tmp.size()), cview); std::fill(begin(tmp), end(tmp), 0); EXPECT_EQ(fs.read(fh, tmp.data(), tmp.size(), ec), cview.size()); EXPECT_EQ(std::string_view(tmp.data(), tmp.size()), cview); EXPECT_FALSE(ec); EXPECT_THROW(fs.read(fh_invalid, tmp.data(), tmp.size()), std::system_error); fs.read(fh_invalid, tmp.data(), tmp.size(), ec); EXPECT_TRUE(ec); EXPECT_EQ(ec.value(), EINVAL); // --- readv --- iov.clear(); EXPECT_EQ(fs.readv(fh, iov), cview.size()); EXPECT_EQ(iov_to_str(iov), cview); iov.clear(); EXPECT_EQ(fs.readv(fh, iov, ec), cview.size()); EXPECT_EQ(iov_to_str(iov), cview); EXPECT_FALSE(ec); EXPECT_THROW(fs.readv(fh_invalid, iov), std::system_error); fs.readv(fh_invalid, iov, ec); EXPECT_TRUE(ec); EXPECT_EQ(ec.value(), EINVAL); // --- readv (async) --- EXPECT_EQ(fut_to_str(fs.readv(fh)), cview); EXPECT_EQ(fut_to_str(fs.readv(fh, ec)), cview); EXPECT_FALSE(ec); EXPECT_THROW(fs.readv(fh_invalid), std::system_error); fs.readv(fh_invalid, ec); EXPECT_TRUE(ec); EXPECT_EQ(ec.value(), EINVAL); for (size_t size : {0, 1, 2, 3, 512, 555, 33333}) { // --- read_string --- EXPECT_EQ(fs.read_string(fh, size), cview.substr(0, size)) << size; EXPECT_EQ(fs.read_string(fh, size, ec), cview.substr(0, size)) << size; EXPECT_FALSE(ec) << size; EXPECT_THROW(fs.read_string(fh_invalid, size), std::system_error) << size; fs.read_string(fh_invalid, size, ec); EXPECT_TRUE(ec) << size; EXPECT_EQ(ec.value(), EINVAL) << size; // --- read --- tmp.resize(size); std::fill(begin(tmp), end(tmp), 0); EXPECT_EQ(fs.read(fh, tmp.data(), tmp.size()), size) << size; EXPECT_EQ(std::string_view(tmp.data(), tmp.size()), cview.substr(0, size)) << size; std::fill(begin(tmp), end(tmp), 0); EXPECT_EQ(fs.read(fh, tmp.data(), tmp.size(), ec), size) << size; EXPECT_EQ(std::string_view(tmp.data(), tmp.size()), cview.substr(0, size)) << size; EXPECT_FALSE(ec) << size; EXPECT_THROW(fs.read(fh_invalid, tmp.data(), tmp.size()), std::system_error) << size; fs.read(fh_invalid, tmp.data(), tmp.size(), ec); EXPECT_TRUE(ec) << size; EXPECT_EQ(ec.value(), EINVAL) << size; // --- readv --- iov.clear(); EXPECT_EQ(fs.readv(fh, iov, size), size) << size; EXPECT_EQ(iov_to_str(iov), cview.substr(0, size)) << size; iov.clear(); EXPECT_EQ(fs.readv(fh, iov, size, ec), size) << size; EXPECT_EQ(iov_to_str(iov), cview.substr(0, size)) << size; EXPECT_FALSE(ec) << size; EXPECT_THROW(fs.readv(fh_invalid, iov, size), std::system_error) << size; fs.readv(fh_invalid, iov, size, ec); EXPECT_TRUE(ec) << size; EXPECT_EQ(ec.value(), EINVAL) << size; // --- readv (async) --- EXPECT_EQ(fut_to_str(fs.readv(fh, size)), cview.substr(0, size)) << size; EXPECT_EQ(fut_to_str(fs.readv(fh, size, ec)), cview.substr(0, size)) << size; EXPECT_FALSE(ec) << size; EXPECT_THROW(fs.readv(fh_invalid, size), std::system_error) << size; fs.readv(fh_invalid, size, ec); EXPECT_TRUE(ec) << size; EXPECT_EQ(ec.value(), EINVAL) << size; for (file_off_t off : {0, 1, 2, 3, 255, 256, 257, 33333}) { // --- read_string --- EXPECT_EQ(fs.read_string(fh, size, off), cview.substr(off, size)) << size << ":" << off; EXPECT_EQ(fs.read_string(fh, size, off, ec), cview.substr(off, size)) << size << ":" << off; EXPECT_FALSE(ec) << size << ":" << off; EXPECT_THROW(fs.read_string(fh_invalid, size, off), std::system_error) << size << ":" << off; fs.read_string(fh_invalid, size, off, ec); EXPECT_TRUE(ec) << size << ":" << off; EXPECT_EQ(ec.value(), EINVAL) << size << ":" << off; // --- read --- std::fill(begin(tmp), end(tmp), 0); EXPECT_EQ(fs.read(fh, tmp.data(), tmp.size(), off), size) << size << ":" << off; EXPECT_EQ(std::string_view(tmp.data(), tmp.size()), cview.substr(off, size)) << size << ":" << off; std::fill(begin(tmp), end(tmp), 0); EXPECT_EQ(fs.read(fh, tmp.data(), tmp.size(), off, ec), size) << size << ":" << off; EXPECT_EQ(std::string_view(tmp.data(), tmp.size()), cview.substr(off, size)) << size << ":" << off; EXPECT_FALSE(ec) << size << ":" << off; EXPECT_THROW(fs.read(fh_invalid, tmp.data(), tmp.size(), off), std::system_error) << size << ":" << off; fs.read(fh_invalid, tmp.data(), tmp.size(), off, ec); EXPECT_TRUE(ec) << size << ":" << off; EXPECT_EQ(ec.value(), EINVAL) << size << ":" << off; // --- readv --- iov.clear(); EXPECT_EQ(fs.readv(fh, iov, size, off), size) << size << ":" << off; EXPECT_EQ(iov_to_str(iov), cview.substr(off, size)) << size << ":" << off; EXPECT_GE(iov.buf.size(), size / 256) << size << ":" << off; iov.clear(); EXPECT_EQ(fs.readv(fh, iov, size, off, ec), size) << size << ":" << off; EXPECT_EQ(iov_to_str(iov), cview.substr(off, size)) << size << ":" << off; EXPECT_FALSE(ec) << size << ":" << off; EXPECT_THROW(fs.readv(fh_invalid, iov, size, off), std::system_error) << size << ":" << off; fs.readv(fh_invalid, iov, size, off, ec); EXPECT_TRUE(ec) << size << ":" << off; EXPECT_EQ(ec.value(), EINVAL) << size << ":" << off; // --- readv (async) --- EXPECT_EQ(fut_to_str(fs.readv(fh, size, off)), cview.substr(off, size)) << size << ":" << off; EXPECT_EQ(fut_to_str(fs.readv(fh, size, off, ec)), cview.substr(off, size)) << size << ":" << off; EXPECT_FALSE(ec) << size << ":" << off; EXPECT_THROW(fs.readv(fh_invalid, size, off), std::system_error) << size << ":" << off; fs.readv(fh_invalid, size, off, ec); EXPECT_TRUE(ec) << size << ":" << off; EXPECT_EQ(ec.value(), EINVAL) << size << ":" << off; } } // --- error/non-error cases --- // read past end of file EXPECT_EQ(fs.read_string(fh, 42, 76530), cview.substr(76530)); iov.clear(); EXPECT_EQ(fs.readv(fh, iov, 42, 76530), 13); EXPECT_EQ(iov_to_str(iov), cview.substr(76530)); // offset past end of file EXPECT_EQ(fs.read_string(fh, 42, 80000), std::string()); iov.clear(); EXPECT_EQ(fs.readv(fh, iov, 42, 80000), 0); EXPECT_EQ(iov_to_str(iov), std::string()); // negative offset fs.read_string(fh, 42, -1, ec); EXPECT_TRUE(ec); EXPECT_EQ(ec.value(), EINVAL); iov.clear(); EXPECT_EQ(fs.readv(fh, iov, 42, -1, ec), 0); EXPECT_TRUE(ec); EXPECT_EQ(ec.value(), EINVAL); }