diff --git a/include/GParted_Core.h b/include/GParted_Core.h index 484e8b18..41afba62 100644 --- a/include/GParted_Core.h +++ b/include/GParted_Core.h @@ -40,6 +40,9 @@ namespace GParted class GParted_Core { +friend class EraseFileSystemSignaturesTest; // To allow unit testing to call private + // method. + public: static Glib::Thread *mainthread; GParted_Core() ; diff --git a/tests/Makefile.am b/tests/Makefile.am index 931c1aef..904c4fa1 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -11,19 +11,17 @@ LDADD = \ # Programs to be built by "make check" check_PROGRAMS = \ - test_dummy \ - test_BlockSpecial \ - test_PasswordRAMStore \ - test_PipeCapture \ + test_dummy \ + test_BlockSpecial \ + test_EraseFileSystemSignatures \ + test_PasswordRAMStore \ + test_PipeCapture \ test_SupportedFileSystems # Test cases to be run by "make check" TESTS = $(check_PROGRAMS) -test_dummy_SOURCES = test_dummy.cc - -test_SupportedFileSystems_SOURCES = test_SupportedFileSystems.cc -test_SupportedFileSystems_LDADD = \ +gparted_core_OBJECTS = \ $(top_builddir)/src/BCache_Info.$(OBJEXT) \ $(top_builddir)/src/BlockSpecial.$(OBJEXT) \ $(top_builddir)/src/CopyBlocks.$(OBJEXT) \ @@ -65,15 +63,21 @@ test_SupportedFileSystems_LDADD = \ $(top_builddir)/src/reiser4.$(OBJEXT) \ $(top_builddir)/src/reiserfs.$(OBJEXT) \ $(top_builddir)/src/udf.$(OBJEXT) \ - $(top_builddir)/src/xfs.$(OBJEXT) \ - $(GTEST_LIBS) \ - $(top_builddir)/lib/gtest/lib/libgtest.la + $(top_builddir)/src/xfs.$(OBJEXT) + +test_dummy_SOURCES = test_dummy.cc test_BlockSpecial_SOURCES = test_BlockSpecial.cc test_BlockSpecial_LDADD = \ $(top_builddir)/src/BlockSpecial.$(OBJEXT) \ $(LDADD) +test_EraseFileSystemSignatures_SOURCES = test_EraseFileSystemSignatures.cc +test_EraseFileSystemSignatures_LDADD = \ + $(gparted_core_OBJECTS) \ + $(GTEST_LIBS) \ + $(top_builddir)/lib/gtest/lib/libgtest.la + test_PasswordRAMStore_SOURCES = test_PasswordRAMStore.cc test_PasswordRAMStore_LDADD = \ $(top_builddir)/src/PasswordRAMStore.$(OBJEXT) \ @@ -83,3 +87,9 @@ test_PipeCapture_SOURCES = test_PipeCapture.cc test_PipeCapture_LDADD = \ $(top_builddir)/src/PipeCapture.$(OBJEXT) \ $(LDADD) + +test_SupportedFileSystems_SOURCES = test_SupportedFileSystems.cc +test_SupportedFileSystems_LDADD = \ + $(gparted_core_OBJECTS) \ + $(GTEST_LIBS) \ + $(top_builddir)/lib/gtest/lib/libgtest.la diff --git a/tests/test_EraseFileSystemSignatures.cc b/tests/test_EraseFileSystemSignatures.cc new file mode 100644 index 00000000..524de71d --- /dev/null +++ b/tests/test_EraseFileSystemSignatures.cc @@ -0,0 +1,388 @@ +/* Copyright (C) 2023 Mike Fleetwood + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, see . + */ + +/* Test GParted_Core::erase_filesystem_signatures() + */ + + +#include "GParted_Core.h" +#include "OperationDetail.h" +#include "Partition.h" +#include "Utils.h" +#include "gtest/gtest.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace GParted +{ + + +// Hacky XML parser which strips italic and bold markup added in +// OperationDetail::set_description() and reverts just these 5 characters &<>'" encoded by +// Glib::Markup::escape_text() -> g_markup_escape_text() -> append_escaped_text(). +Glib::ustring strip_markup(const Glib::ustring& str) +{ + size_t len = str.length(); + size_t i = 0; + Glib::ustring ret; + ret.reserve(len); + while (i < len) + { + if (str.compare(i, 3, "") == 0) + i += 3; + else if (str.compare(i, 4, "") == 0) + i += 4; + else if (str.compare(i, 3, "") == 0) + i += 3; + else if (str.compare(i, 4, "") == 0) + i += 4; + else if (str.compare(i, 5, "&") == 0) + { + ret.push_back('&'); + i += 5; + } + else if (str.compare(i, 4, "<") == 0) + { + ret.push_back('<'); + i += 4; + } + else if (str.compare(i, 4, ">") == 0) + { + ret.push_back('>'); + i += 4; + } + else if (str.compare(i, 6, "'") == 0) + { + ret.push_back('\''); + i += 6; + } + else if (str.compare(i, 6, """) == 0) + { + ret.push_back('"'); + i += 6; + } + else + { + ret.push_back(str[i]); + i++; + } + } + return ret; +} + + +// Print method for OperationDetailStatus. +std::ostream& operator<<(std::ostream& out, const OperationDetailStatus od_status) +{ + switch (od_status) + { + case STATUS_NONE: out << "NONE"; break; + case STATUS_EXECUTE: out << "EXECUTE"; break; + case STATUS_SUCCESS: out << "SUCCESS"; break; + case STATUS_ERROR: out << "ERROR"; break; + case STATUS_INFO: out << "INFO"; break; + case STATUS_WARNING: out << "WARNING"; break; + default: break; + } + return out; +} + + +// Print method for an OperationDetail object. +std::ostream& operator<<(std::ostream& out, const OperationDetail& od) +{ + out << strip_markup(od.get_description()); + Glib::ustring elapsed = od.get_elapsed_time(); + if (! elapsed.empty()) + out << " " << elapsed; + if (od.get_status() != STATUS_NONE) + out << " (" << od.get_status() << ")"; + out << "\n"; + + for (size_t i = 0; i < od.get_childs().size(); i++) + { + out << *od.get_childs()[i]; + } + return out; +} + + +// Explicit test fixture class for common variables and methods used in each test. +// Reference: +// Google Test, Primer, Test Fixtures: Using the Same Data Configuration for Multiple Tests +class EraseFileSystemSignaturesTest : public ::testing::Test +{ +protected: + virtual void create_image_file(Byte_Value size); + virtual void write_intel_software_raid_signature(); + virtual bool image_contains_all_zeros(); + virtual void TearDown(); + + bool erase_filesystem_signatures(const Partition& partition, OperationDetail& operationdetail) + { return m_gparted_core.erase_filesystem_signatures(partition, operationdetail); }; + + static const char* s_image_name; + + GParted_Core m_gparted_core; + Partition m_partition; + OperationDetail m_operation_detail; +}; + + +const char* EraseFileSystemSignaturesTest::s_image_name = "test_EraseFileSystemSignatures.img"; + + +void EraseFileSystemSignaturesTest::create_image_file(Byte_Value size) +{ + // Create new image file to work with. + unlink(s_image_name); + int fd = open(s_image_name, O_WRONLY|O_CREAT|O_NONBLOCK, 0666); + ASSERT_GE(fd, 0) << "Failed to create image file '" << s_image_name << "'. errno=" + << errno << "," << strerror(errno); + ASSERT_EQ(ftruncate(fd, (off_t)size), 0) << "Failed to set image file '" << s_image_name << "' to size " + << size << ". errno=" << errno << "," << strerror(errno); + close(fd); + + // Initialise m_partition as a Partition object spanning the whole of the image file. + m_partition.Reset(); + + PedDevice* lp_device = ped_device_get(s_image_name); + ASSERT_TRUE(lp_device != NULL); + + m_partition.set_unpartitioned(s_image_name, + lp_device->path, + FS_UNALLOCATED, + lp_device->length, + lp_device->sector_size, + false); + + ped_device_destroy(lp_device); + lp_device = NULL; +} + + +void EraseFileSystemSignaturesTest::write_intel_software_raid_signature() +{ + int fd = open(s_image_name, O_WRONLY|O_NONBLOCK); + ASSERT_GE(fd, 0) << "Failed to open image file '" << s_image_name << "'. errno=" + << errno << "," << strerror(errno); + const char* signature = "Intel Raid ISM Cfg Sig. "; + size_t len_signature = strlen(signature); + + // Write Intel Software RAID signature at -2 sectors before the end. Hard codes + // sector size to 512 bytes for a file. + // Reference: + // .../util-linux/libblkid/src/superblocks/isw_raid.c:probe_iswraid(). + ASSERT_GE(lseek(fd, 2 * -512, SEEK_END), 0) << "Failed to seek in image file '" << s_image_name + << "'. errno=" << errno << "," << strerror(errno); + ASSERT_EQ(write(fd, signature, len_signature), (ssize_t)len_signature) + << "Failed to write to image file '" << s_image_name << "'. errno=" + << errno << "," << strerror(errno); + close(fd); +} + + +const char* first_non_zero_byte(const char* buf, size_t size) +{ + while (size > 0) + { + if (*buf != '\0') + return buf; + buf++; + size--; + } + return NULL; +} + + +// Number of bytes of binary data to report. +const size_t BinaryStringChunkSize = 16; + + +// Format up to BinaryStringChunkSize (16) bytes of binary data ready for printing as: +// Hex offset ASCII text Hex bytes +// "0x000000000 \"ABCDEFGHabcdefgh\" 41 42 43 44 45 46 47 48 61 62 63 64 65 66 67 68" +std::string binary_string_to_print(size_t offset, const char* s, size_t len) +{ + std::ostringstream result; + + result << "0x"; + result.fill('0'); + result << std::setw(8) << std::hex << std::uppercase << offset << " \""; + + size_t i; + for (i = 0; i < BinaryStringChunkSize && i < len; i++) + result.put((isprint(s[i])) ? s[i] : '.'); + result.put('\"'); + + if (len > 0) + { + for (; i < BinaryStringChunkSize; i++) + result.put(' '); + result.put(' '); + + for (i = 0 ; i < BinaryStringChunkSize && i < len; i++) + result << " " + << std::setw(2) << std::hex << std::uppercase + << (unsigned int)(unsigned char)s[i]; + } + + return result.str(); +} + + +bool EraseFileSystemSignaturesTest::image_contains_all_zeros() +{ + int fd = open(s_image_name, O_RDONLY|O_NONBLOCK); + if (fd < 0) + { + ADD_FAILURE() << __func__ << "(): Failed to open image file '" << s_image_name << "'. errno=" + << errno << "," << strerror(errno); + return false; + } + + ssize_t bytes_read = 0; + size_t offset = 0; + do + { + char buf[BUFSIZ]; + bytes_read = read(fd, buf, sizeof(buf)); + if (bytes_read < 0) + { + ADD_FAILURE() << __func__ << "(): Failed to read from image file '" << s_image_name + << "'. errno=" << errno << "," << strerror(errno); + close(fd); + return false; + } + const char* p = first_non_zero_byte(buf, bytes_read); + if (p != NULL) + { + ADD_FAILURE() << __func__ << "(): First non-zero bytes:\n" + << binary_string_to_print(offset + (p - buf), p, buf + bytes_read - p); + close(fd); + return false; + } + } + while (bytes_read > 0); + close(fd); + return true; +} + + +void EraseFileSystemSignaturesTest::TearDown() +{ + unlink(s_image_name); +} + + +TEST_F(EraseFileSystemSignaturesTest, IntelSoftwareRAIDAligned) +{ + create_image_file(16 * MEBIBYTE); + write_intel_software_raid_signature(); + + EXPECT_TRUE(erase_filesystem_signatures(m_partition, m_operation_detail)) << m_operation_detail; + EXPECT_TRUE(image_contains_all_zeros()); +} + + +TEST_F(EraseFileSystemSignaturesTest, IntelSoftwareRAIDUnaligned) +{ + create_image_file(16 * MEBIBYTE - 512); + write_intel_software_raid_signature(); + + EXPECT_TRUE(erase_filesystem_signatures(m_partition, m_operation_detail)) << m_operation_detail; + EXPECT_TRUE(image_contains_all_zeros()); +} + + +} // namespace GParted + + +// Re-execute current executable using xvfb-run so that it provides a virtual X11 display. +void exec_using_xvfb_run(int argc, char** argv) +{ + // argc+2 = Space for "xvfb-run" command, existing argc strings plus NULL pointer. + size_t size = sizeof(char*) * (argc+2); + char** new_argv = (char**)malloc(size); + if (new_argv == NULL) + { + fprintf(stderr, "Failed to allocate %lu bytes of memory. errno=%d,%s\n", + (unsigned long)size, errno, strerror(errno)); + exit(EXIT_FAILURE); + } + + new_argv[0] = strdup("xvfb-run"); + if (new_argv[0] == NULL) + { + fprintf(stderr, "Failed to allocate %lu bytes of memory. errno=%d,%s\n", + (unsigned long)strlen(new_argv[0])+1, errno, strerror(errno)); + exit(EXIT_FAILURE); + } + + // Copy argv pointers including final NULL pointer. + for (unsigned int i = 0; i <= (unsigned)argc; i++) + new_argv[i+1] = argv[i]; + + execvp(new_argv[0], new_argv); + fprintf(stderr, "Failed to execute '%s %s ...'. errno=%d,%s\n", new_argv[0], new_argv[1], + errno, strerror(errno)); + exit(EXIT_FAILURE); +} + + +// Custom Google Test main(). +// Reference: +// * Google Test, Primer, Writing the main() function +// https://github.com/google/googletest/blob/master/googletest/docs/primer.md#writing-the-main-function +int main(int argc, char** argv) +{ + printf("Running main() from %s\n", __FILE__); + + const char* display = getenv("DISPLAY"); + if (display == NULL) + { + printf("DISPLAY environment variable unset. Executing 'xvfb-run %s ...'\n", argv[0]); + exec_using_xvfb_run(argc, argv); + } + printf("DISPLAY=\"%s\"\n", display); + + // Initialise threading in GParted to successfully use Utils:: and + // FileSystem::execute_command(). Must be before InitGoogleTest(). + GParted::GParted_Core::mainthread = Glib::Thread::self(); + Gtk::Main gtk_main = Gtk::Main(); + + testing::InitGoogleTest(&argc, argv); + + return RUN_ALL_TESTS(); +}