// Copyright (c) 2025 Zano Project // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "epee/include/include_base_utils.h" #include "crypto/crypto.h" #include "crypto/crypto-sugar.h" #include "gtest/gtest.h" #include #include #include #include #include "common/pod_array_file_container.h" // helper: returns a unique temp file path static boost::filesystem::path make_temp_file() { return boost::filesystem::temp_directory_path() / boost::filesystem::unique_path("pod_test_%%%%-%%%%.bin"); } //====================================================================== // typed test fixture for pod_array_file_container // container = alias for pod_array_file_container // tmp_path = temp file for test // SetUp() = generate temp path (SetUp from ::testing::Test) // TearDown() = remove temp file (TearDown from ::testing::Test) //====================================================================== template class pod_array_file_typed_test : public ::testing::Test { protected: using container = tools::pod_array_file_container; boost::filesystem::path tmp_path; void SetUp() override { tmp_path = make_temp_file(); } void TearDown() override { if (boost::filesystem::exists(tmp_path)) boost::filesystem::remove(tmp_path); } }; // list of integral types to test using integral_types = ::testing::Types< int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t >; // register typed tests TYPED_TEST_CASE(pod_array_file_typed_test, integral_types); // push back and get items: // write values [-1,0,1] and verify they are read back correctly TYPED_TEST(pod_array_file_typed_test, push_back_and_get_items) { typename TestFixture::container c; ASSERT_TRUE(c.open(this->tmp_path.wstring(), true)); std::vector values = { static_cast(-1), 0, static_cast(1) }; for (auto v : values) ASSERT_TRUE(c.push_back(v)); EXPECT_EQ(c.size(), values.size()); TypeParam read_value; for (size_t i = 0; i < values.size(); ++i) { ASSERT_TRUE(c.get_item(i, read_value)); EXPECT_EQ(read_value, values[i]); } } // ensure get_item returns false for index >= size TYPED_TEST(pod_array_file_typed_test, get_item_out_of_range) { typename TestFixture::container c; ASSERT_TRUE(c.open(this->tmp_path.wstring(), true)); TypeParam dummy; EXPECT_FALSE(c.get_item(0, dummy)); EXPECT_FALSE(c.get_item(100, dummy)); } // -------------------------------------------------------------------- typedef uint32_t pod_t; typedef tools::pod_array_file_container pod_container; // open fails if not exist without create: // open() false when file missing and create_if_not_exist=false TEST(pod_array_file_container, open_fails_if_not_exist_without_create) { auto path = make_temp_file(); pod_container c; std::string reason; bool opened = c.open(path.wstring(), false, nullptr, &reason); EXPECT_FALSE(opened); EXPECT_TRUE(reason.find("not exist") != std::string::npos); } // open creates file when not exist: // open() with create flag creates empty file and size=0 TEST(pod_array_file_container, open_creates_file_when_not_exist) { auto path = make_temp_file(); pod_container c; bool corrupted = true; ASSERT_TRUE(c.open(path.wstring(), true, &corrupted, nullptr)); EXPECT_FALSE(corrupted); EXPECT_EQ(c.size(), 0u); c.close(); EXPECT_TRUE(boost::filesystem::exists(path)); } // -------------------------------------------------------------------- // POD struct with fixed-size fields struct test_struct { crypto::public_key pubkey; crypto::key_image key; }; typedef tools::pod_array_file_container struct_container; // push back and get items struct: // write two test_struct and verify memory equality TEST(pod_array_file_container_struct, push_back_and_get_items_struct) { auto path = make_temp_file(); struct_container c; ASSERT_TRUE(c.open(path.wstring(), true)); test_struct a1{}, a2{}; // use random values for pubkey and key a1.pubkey = (crypto::scalar_t::random() * crypto::c_point_G).to_public_key(); a1.key = (crypto::scalar_t::random() * crypto::c_point_G).to_key_image(); a2.pubkey = (crypto::scalar_t::random() * crypto::c_point_G).to_public_key(); a2.key = (crypto::scalar_t::random() * crypto::c_point_G).to_key_image(); ASSERT_TRUE(c.push_back(a1)); ASSERT_TRUE(c.push_back(a2)); EXPECT_EQ(c.size(), 2u); test_struct got; ASSERT_TRUE(c.get_item(0, got)); EXPECT_EQ(got.pubkey, a1.pubkey); EXPECT_EQ(got.key, a1.key); ASSERT_TRUE(c.get_item(1, got)); EXPECT_EQ(got.pubkey, a2.pubkey); EXPECT_EQ(got.key, a2.key); } // get item out of range struct: // get_item false when no items written TEST(pod_array_file_container_struct, get_item_out_of_range_struct) { auto path = make_temp_file(); struct_container c; ASSERT_TRUE(c.open(path.wstring(), true)); test_struct dummy; EXPECT_FALSE(c.get_item(0, dummy)); } // corrupted file truncation struct: // simulate corrupted file tail and check truncation flag and size TEST(pod_array_file_container_struct, corrupted_file_truncation_struct) { auto path = make_temp_file(); { // write a valid test_struct followed by garbage data boost::filesystem::ofstream out(path, std::ios::binary | std::ios::out); test_struct tmp{}; std::fill(std::begin(tmp.pubkey.data), std::end(tmp.pubkey.data), 'X'); std::fill(std::begin(tmp.key.data), std::end(tmp.key.data), 'Y'); out.write(reinterpret_cast(&tmp), sizeof(tmp)); const char garbage[5] = {1,2,3,4,5}; out.write(garbage, sizeof(garbage)); } struct_container c; bool corrupted = false; std::string reason; ASSERT_TRUE(c.open(path.wstring(), false, &corrupted, &reason)); EXPECT_TRUE(corrupted); EXPECT_EQ(c.size(), 1u); test_struct got; ASSERT_TRUE(c.get_item(0, got)); for (size_t i = 0; i < sizeof(got.pubkey.data); ++i) EXPECT_EQ(got.pubkey.data[i], 'X'); for (size_t i = 0; i < sizeof(got.key.data); ++i) EXPECT_EQ(got.key.data[i], 'Y'); EXPECT_TRUE(reason.find("truncated") != std::string::npos); } // persistence between opens struct: // write multiple structs, reopen file, verify data persists TEST(pod_array_file_container_struct, persistence_between_opens_struct) { auto path = make_temp_file(); { // write 3 test_struct with different pubkey and key values struct_container c; ASSERT_TRUE(c.open(path.wstring(), true)); for (int i = 0; i < 3; ++i) { test_struct tmp{}; tmp.pubkey.data[0] = '0' + i; tmp.key.data[0] = 'A' + i; ASSERT_TRUE(c.push_back(tmp)); } EXPECT_EQ(c.size(), 3u); } // reopen and verify data struct_container c2; ASSERT_TRUE(c2.open(path.wstring(), false)); EXPECT_EQ(c2.size(), 3u); test_struct got; for (int i = 0; i < 3; ++i) { ASSERT_TRUE(c2.get_item(i, got)); EXPECT_EQ(got.pubkey.data[0], '0' + i); EXPECT_EQ(got.key.data[0], 'A' + i); } } // size bytes and size: // check size_bytes() matches raw byte count and size() element count TEST(pod_array_file_container, size_bytes_and_size) { auto path = make_temp_file(); pod_container c; ASSERT_TRUE(c.open(path.wstring(), true)); EXPECT_EQ(c.size_bytes(), 0u); EXPECT_EQ(c.size(), 0u); // push one element pod_t value = 42; ASSERT_TRUE(c.push_back(value)); EXPECT_EQ(c.size_bytes(), sizeof(pod_t)); EXPECT_EQ(c.size(), 1u); } // operations after close: // ensure push_back and get_item fail after close() TEST(pod_array_file_container, operations_after_close) { auto path = make_temp_file(); pod_container c; ASSERT_TRUE(c.open(path.wstring(), true)); ASSERT_TRUE(c.push_back(123u)); c.close(); // after close, operations should return false EXPECT_FALSE(c.push_back(456u)); pod_t dummy = 0; EXPECT_FALSE(c.get_item(0, dummy)); } // open fails if cannot open (directory): // attempt to open a directory path should fail with "file could not be opened" TEST(pod_array_file_container, open_fails_if_cannot_open) { // create a directory instead of a file auto dir_path = make_temp_file(); boost::filesystem::create_directory(dir_path); pod_container c; std::string reason; bool opened = c.open(dir_path.wstring(), true, nullptr, &reason); EXPECT_FALSE(opened); EXPECT_TRUE(reason.find("could not be opened") != std::string::npos); boost::filesystem::remove(dir_path); } // corrupted file truncation uint32: // simulate corrupted file tail on uint32_t and check truncation TEST(pod_array_file_container, corrupted_file_truncation_uint32) { auto path = make_temp_file(); { boost::filesystem::ofstream out(path, std::ios::binary | std::ios::out); pod_t v = 0x12345678u; out.write(reinterpret_cast(&v), sizeof(v)); const char junk[3] = {9,8,7}; out.write(junk, sizeof(junk)); } pod_container c; bool corrupted = false; std::string reason; ASSERT_TRUE(c.open(path.wstring(), false, &corrupted, &reason)); EXPECT_TRUE(corrupted); EXPECT_EQ(c.size(), 1u); pod_t read_v; ASSERT_TRUE(c.get_item(0, read_v)); EXPECT_EQ(read_v, 0x12345678u); EXPECT_TRUE(reason.find("truncated") != std::string::npos); } // operations without open: // ensure push_back/get_item/size_bytes/size behave when container never opened TEST(pod_array_file_container, operations_without_open) { pod_container c; EXPECT_FALSE(c.push_back(1u)); pod_t dummy; EXPECT_FALSE(c.get_item(0, dummy)); EXPECT_EQ(c.size_bytes(), 0u); EXPECT_EQ(c.size(), 0u); } // checks stream state transitions TEST(pod_array_file_container, is_opened_and_in_good_state) { auto path = make_temp_file(); pod_container c; // not opened yet EXPECT_FALSE(c.is_opened_and_in_good_state()); // open for create ASSERT_TRUE(c.open(path.wstring(), true)); EXPECT_TRUE(c.is_opened_and_in_good_state()); // after close c.close(); EXPECT_FALSE(c.is_opened_and_in_good_state()); } // wipes file contents and resets size TEST(pod_array_file_container, clear_resets_file) { auto path = make_temp_file(); pod_container c; ASSERT_TRUE(c.open(path.wstring(), true)); // add some elements ASSERT_TRUE(c.push_back(123u)); ASSERT_TRUE(c.push_back(456u)); EXPECT_EQ(c.size(), 2u); // clear the container ASSERT_TRUE(c.clear()); EXPECT_EQ(c.size(), 0u); // file should still be usable ASSERT_TRUE(c.push_back(789u)); EXPECT_EQ(c.size(), 1u); }