diff --git a/src/common/pod_array_file_container.h b/src/common/pod_array_file_container.h index 8ac36270..e165480c 100644 --- a/src/common/pod_array_file_container.h +++ b/src/common/pod_array_file_container.h @@ -137,12 +137,9 @@ namespace tools // close and re-open stream with trunc bit m_stream.close(); - m_stream.open(m_filename, std::ios::binary | std::ios::trunc | std::ios::in); + m_stream.open(m_filename, std::ios::binary | std::ios::trunc | std::ios::in | std::ios::out); - if (m_stream.rdstate() != std::ios::eofbit) - return false; - - return true; + return is_opened_and_in_good_state(); } private: diff --git a/tests/unit_tests/pod_array_file_container.cpp b/tests/unit_tests/pod_array_file_container.cpp new file mode 100644 index 00000000..ba4615aa --- /dev/null +++ b/tests/unit_tests/pod_array_file_container.cpp @@ -0,0 +1,364 @@ +// 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); +} \ No newline at end of file