/* Copyright (C) 2017 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 <http://www.gnu.org/licenses/>.
 */

/* Test BlockSpecial
 *
 * In order to make the tests runable by non-root users in a wide
 * variety of environments as possible the tests try to assume as small
 * a set of available files as possible and discovers used block device
 * names and symbolic link name.  The following files are explicitly
 * needed for the tests:
 *
 *     Name                      Access    Note
 *     -----------------------   -------   -----
 *     /                         Stat
 *     /proc/partitions          Stat
 *                               Read      To find any two block
 *                                         devices
 *     /dev/BLOCK0               Stat      First entry from
 *                                         /proc/partitions
 *     /dev/BLOCK1               stat      Second entry from
 *                                         /proc/partitions
 *     /dev/disk/by-id/          Readdir   To find any symlink to a
 *                                         block device
 *     /dev/disk/by-id/SYMLINK   Stat      First directory entry
 *     /dev/RBLOCK               Stat      Device to which SYMLINK
 *                                         refers
 *
 * Other dummy names are pre-loaded into the internal BlockSpecial cache
 * and never queried from the file system.
 */

#include "BlockSpecial.h"
#include "gtest/gtest.h"

#include <string>
#include <iostream>
#include <fstream>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <dirent.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <glibmm/ustring.h>


namespace GParted
{

// Print method for a BlockSpecial object
std::ostream& operator<<( std::ostream & out, const BlockSpecial & bs )
{
	out << "BlockSpecial{\"" << bs.m_name << "\"," << bs.m_major << "," << bs.m_minor << "}";
	return out;
}

// Helper to construct and return message for equality assertion used in:
//     EXPECT_BSEQTUP( bs, name, major, minor )
// Reference:
//     Google Test, AdvancedGuide, Using a Predicate-Formatter
::testing::AssertionResult CompareHelperBS2TUP( const char * bs_expr, const char * name_expr,
                                                const char * major_expr, const char * minor_expr,
                                                const BlockSpecial & bs, const Glib::ustring & name,
                                                unsigned long major, unsigned long minor )
{
	if ( bs.m_name == name && bs.m_major == major && bs.m_minor == minor )
		return ::testing::AssertionSuccess();
	else
		return ::testing::AssertionFailure()
		       << "      Expected: " << bs_expr << "\n"
		       << "      Which is: " << bs << "\n"
		       << "To be equal to: (" << name_expr << "," << major_expr << "," << major_expr << ")";
}

// Nonfatal assertion that BlockSpecial is equal to tuple (name, major, minor)
#define EXPECT_BSEQTUP(bs, name, major, minor)  \
	EXPECT_PRED_FORMAT4(CompareHelperBS2TUP, bs, name, major, minor)

// Helper to print two compared BlockSpecial objects on failure.
// Usage:
//     EXPECT_TRUE( bs1 == bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
#define ON_FAILURE_WHERE(b1, b2)  \
	"   Where: " << #b1 << " = " << (b1) << "\n"  \
	"     And: " << #b2 << " = " << (b2);


#define SKIP_IF_BLOCK_DEVICE_DOESNT_EXIST(name)                                      \
{                                                                                    \
	struct stat sb;                                                              \
	if (stat(name.c_str(), &sb) != 0 && errno == ENOENT)                         \
	{                                                                            \
		std::cout << __FILE__ << ":" << __LINE__ << ": Skip test.  Block "   \
		          << "device '" << name << "' does not exist" << std::endl;  \
		return;                                                              \
	}                                                                            \
}


// Return block device names numbered 0 upwards like "/dev/sda" by reading entries from
// /proc/partitions.
static std::string get_block_name( unsigned want )
{
	std::ifstream proc_partitions( "/proc/partitions" );
	if ( ! proc_partitions )
	{
		ADD_FAILURE() << __func__ << "(): Failed to open '/proc/partitions'";
		return "";
	}

	std::string line;
	char name[100] = "";
	unsigned entry = 0;
	while ( getline( proc_partitions, line ) )
	{
		if ( sscanf( line.c_str(), "%*u %*u %*u %99s", name ) == 1 )
		{
			if ( entry == want )
				break;
			entry ++;
		}
	}
	proc_partitions.close();

	if ( entry == want )
		return std::string( "/dev/" ) + name;

	ADD_FAILURE() << __func__ << "(): Entry " << want << " not found in '/proc/partitions'";
	return "";
}

// Return symbolic link to a block device by reading the first entry in the directory
// /dev/disk/by-id/.
static std::string get_link_name()
{
	DIR * dir = opendir( "/dev/disk/by-id" );
	if (dir == nullptr)
	{
		ADD_FAILURE() << __func__ << "(): Failed to open directory '/dev/disk/by-id'";
		return "";
	}

	std::string name;
	bool found = false;
	struct dirent * dentry;
	// Silence GCC [-Wparentheses] warning with double parentheses
	while ( ( dentry = readdir( dir ) ) )
	{
		if ( strcmp( dentry->d_name, "." )  != 0 &&
		     strcmp( dentry->d_name, ".." ) != 0    )
		{
			name = dentry->d_name;
			found = true;
			break;
		}
	}
	closedir( dir );

	if ( found )
		return std::string("/dev/disk/by-id/") + name;

	ADD_FAILURE() << __func__ << "(): No entries found in directory '/dev/disk/by-id'";
	return "";
}

// Follow symbolic link return real path.
static std::string follow_link_name( std::string link )
{
	char* rpath = realpath(link.c_str(), nullptr);
	if (rpath == nullptr)
	{
		ADD_FAILURE() << __func__ << "(): Failed to resolve symbolic link '" << link << "'";
		return "";
	}

	std::string rpath_copy = rpath;
	free( rpath );
	return rpath_copy;
}

TEST( BlockSpecialTest, UnnamedBlockSpecialObject )
{
	// Test default constructor produces empty BlockSpecial object ("", 0, 0).
	BlockSpecial::clear_cache();
	BlockSpecial bs;
	EXPECT_BSEQTUP( bs, "", 0, 0 );
}

TEST( BlockSpecialTest, NamedBlockSpecialObjectPlainFile )
{
	// Test any named plain file or directory (actually any non-block special name)
	// produces BlockSpecial object (name, 0, 0).
	BlockSpecial::clear_cache();
	BlockSpecial bs( "/" );
	EXPECT_BSEQTUP( bs, "/", 0, 0 );
}

TEST( BlockSpecialTest, NamedBlockSpecialObjectPlainFileDuplicate )
{
	// Test second constructed BlockSpecial object with the same named plain file or
	// directory, where the major and minor numbers are retrieved via internal
	// BlockSpecial cache, produce an equivalent object (name, 0, 0).
	BlockSpecial::clear_cache();
	BlockSpecial bs1( "/" );
	BlockSpecial bs2( "/" );
	EXPECT_BSEQTUP( bs1, "/", 0, 0 );
	EXPECT_BSEQTUP( bs2, "/", 0, 0 );
}

TEST( BlockSpecialTest, NamedBlockSpecialObjectBlockDevice )
{
	std::string bname = get_block_name( 0 );
	SKIP_IF_BLOCK_DEVICE_DOESNT_EXIST(bname);

	// Test any block special name produces BlockSpecial object (name, major, minor).
	BlockSpecial::clear_cache();
	BlockSpecial bs( bname );
	EXPECT_STREQ( bs.m_name.c_str(), bname.c_str() );
	EXPECT_TRUE( bs.m_major > 0 || bs.m_minor > 0 );
}

TEST( BlockSpecialTest, NamedBlockSpecialObjectBlockDeviceDuplicate )
{
	std::string bname = get_block_name( 0 );
	SKIP_IF_BLOCK_DEVICE_DOESNT_EXIST(bname);

	// Test that a second object with the same name of a block device produces the
	// same (name, major, minor).  Checking internal BlockSpecial caching again.
	BlockSpecial::clear_cache();
	BlockSpecial bs1( bname );
	BlockSpecial bs2( bname );
	EXPECT_STREQ( bs1.m_name.c_str(), bs2.m_name.c_str() );
	EXPECT_EQ( bs1.m_major, bs2.m_major );
	EXPECT_EQ( bs1.m_minor, bs2.m_minor );
}

TEST( BlockSpecialTest, TwoNamedBlockSpecialObjectBlockDevices )
{
	std::string bname1 = get_block_name( 0 );
	SKIP_IF_BLOCK_DEVICE_DOESNT_EXIST(bname1);
	std::string bname2 = get_block_name( 1 );
	SKIP_IF_BLOCK_DEVICE_DOESNT_EXIST(bname2);

	// Test that two different named block devices produce different
	// (name, major, minor).
	BlockSpecial::clear_cache();
	BlockSpecial bs1( bname1 );
	BlockSpecial bs2( bname2 );
	EXPECT_STRNE( bs1.m_name.c_str(), bs2.m_name.c_str() );
	EXPECT_TRUE( bs1.m_major != bs2.m_major || bs1.m_minor != bs2.m_minor );
}

TEST( BlockSpecialTest, NamedBlockSpecialObjectBySymlinkMatches )
{
	std::string lname = get_link_name();
	SKIP_IF_BLOCK_DEVICE_DOESNT_EXIST(lname);
	std::string bname = follow_link_name( lname );
	SKIP_IF_BLOCK_DEVICE_DOESNT_EXIST(bname);

	// Test that a symbolic link to a block device and the named block device produce
	// BlockSpecial objects with different names but the same major, minor pair.
	BlockSpecial::clear_cache();
	BlockSpecial lnk( lname );
	BlockSpecial bs( bname );
	EXPECT_STRNE( lnk.m_name.c_str(), bs.m_name.c_str() );
	EXPECT_EQ( lnk.m_major, bs.m_major );
	EXPECT_EQ( lnk.m_minor, bs.m_minor );
}

TEST( BlockSpecialTest, PreRegisteredCacheUsedBeforeFileSystem )
{
	// Test that the cache is used before querying the file system by registering a
	// deliberately wrong major, minor pair for /dev/null and ensuring those are the
	// major, minor numbers reported for the device.
	BlockSpecial::clear_cache();
	BlockSpecial::register_block_special( "/dev/null", 4, 8 );
	BlockSpecial bs( "/dev/null" );
	EXPECT_BSEQTUP( bs, "/dev/null", 4, 8 );
}

TEST( BlockSpecialTest, OperatorEqualsTwoEmptyObjects )
{
	// Test equality of two empty BlockSpecial objects.
	BlockSpecial::clear_cache();
	BlockSpecial bs1;
	BlockSpecial bs2;
	EXPECT_TRUE( bs1 == bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
}

TEST( BlockSpecialTest, OperatorEqualsSamePlainFiles )
{
	// Test equality of two named plain file or directory BlockSpecial objects;
	BlockSpecial::clear_cache();
	BlockSpecial bs1( "/" );
	BlockSpecial bs2( "/" );
	EXPECT_TRUE( bs1 == bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
}

TEST( BlockSpecialTest, OperatorEqualsEmptyObjectAndPlainFile )
{
	// Test inequality of empty and plain file BlockSpecial objects.
	BlockSpecial::clear_cache();
	BlockSpecial bs1;
	BlockSpecial bs2( "/" );
	EXPECT_FALSE( bs1 == bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
}

TEST( BlockSpecialTest, OperatorEqualsDifferentPlainFiles )
{
	// Test inequality of two different plain file or directory BlockSpecial objects;
	BlockSpecial::clear_cache();
	BlockSpecial bs1( "/" );
	BlockSpecial bs2( "/proc/partitions" );
	EXPECT_FALSE( bs1 == bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
}

TEST( BlockSpecialTest, OperatorEqualsSameBlockDevices )
{
	std::string bname = get_block_name( 0 );
	SKIP_IF_BLOCK_DEVICE_DOESNT_EXIST(bname);

	// Test equality of two BlockSpecial objects using the same named block device.
	BlockSpecial::clear_cache();
	BlockSpecial bs1( bname );
	BlockSpecial bs2( bname );
	EXPECT_TRUE( bs1 == bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
}

TEST( BlockSpecialTest, OperatorEqualsEmptyObjectAndBlockDevice )
{
	std::string bname = get_block_name( 0 );
	SKIP_IF_BLOCK_DEVICE_DOESNT_EXIST(bname);

	// Test inequality of empty and named block device BlockSpecial objects.
	BlockSpecial::clear_cache();
	BlockSpecial bs1;
	BlockSpecial bs2( bname );
	EXPECT_FALSE( bs1 == bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
}

TEST( BlockSpecialTest, OperatorEqualsPlainFileAndBlockDevice )
{
	std::string bname = get_block_name( 0 );
	SKIP_IF_BLOCK_DEVICE_DOESNT_EXIST(bname);

	// Test inequality of plain file and block device BlockSpecial objects.
	BlockSpecial::clear_cache();
	BlockSpecial bs1( "/" );
	BlockSpecial bs2( bname );
	EXPECT_FALSE( bs1 == bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
}

TEST( BlockSpecialTest, OperatorEqualsTwoDifferentBlockDevices )
{
	std::string bname1 = get_block_name( 0 );
	SKIP_IF_BLOCK_DEVICE_DOESNT_EXIST(bname1);
	std::string bname2 = get_block_name( 1 );
	SKIP_IF_BLOCK_DEVICE_DOESNT_EXIST(bname2);

	// Test inequality of two different named block device BlockSpecial objects.
	BlockSpecial::clear_cache();
	BlockSpecial bs1( bname1 );
	BlockSpecial bs2( bname2 );
	EXPECT_FALSE( bs1 == bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
}

TEST( BlockSpecialTest, OperatorEqualsSameBlockDevicesWithMinorZero )
{
	// Test for fix in bug #771670.  Ensure that block devices with minor number 0 are
	// compared by major, minor pair rather than by name.
	BlockSpecial::clear_cache();
	BlockSpecial::register_block_special( "/dev/mapper/encrypted_swap", 254, 0 );
	BlockSpecial::register_block_special( "/dev/dm-0", 254, 0 );
	BlockSpecial bs1( "/dev/mapper/encrypted_swap" );
	BlockSpecial bs2( "/dev/dm-0" );
	EXPECT_TRUE( bs1 == bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
}

TEST( BlockSpecialTest, OperatorLessThanTwoEmptyObjects )
{
	// Test one empty BlockSpecial object is not ordered before another empty one.
	BlockSpecial::clear_cache();
	BlockSpecial bs1;
	BlockSpecial bs2;
	EXPECT_FALSE( bs1 < bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
}

TEST( BlockSpecialTest, OperatorLessThanSamePlainFiles )
{
	// Test one plain file BlockSpecial object is not ordered before another of the
	// same name.
	BlockSpecial::clear_cache();
	BlockSpecial bs1( "/" );
	BlockSpecial bs2( "/" );
	EXPECT_FALSE( bs1 < bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
}

TEST( BlockSpecialTest, OperatorLessThanDifferentPlainFiles )
{
	// Test one plain file BlockSpecial object with the lower name is ordered before
	// the other plain file with a higher name.
	BlockSpecial::clear_cache();
	BlockSpecial::register_block_special( "/dummy_file1", 0, 0 );
	BlockSpecial::register_block_special( "/dummy_file2", 0, 0 );
	BlockSpecial bs1( "/dummy_file1" );
	BlockSpecial bs2( "/dummy_file2" );
	EXPECT_TRUE( bs1 < bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
}

TEST( BlockSpecialTest, OperatorLessThanEmptyObjectAndPlainFile )
{
	// Test empty BlockSpecial object with name "" is before a plain file of any name.
	BlockSpecial::clear_cache();
	BlockSpecial bs1;
	BlockSpecial bs2( "/" );
	EXPECT_TRUE( bs1 < bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
}

TEST( BlockSpecialTest, OperatorLessThanSameBlockDevices )
{
	std::string bname = get_block_name( 0 );
	SKIP_IF_BLOCK_DEVICE_DOESNT_EXIST(bname);

	// Test one name block device BlockSpecial object is not ordered before another
	// from the same name.
	BlockSpecial::clear_cache();
	BlockSpecial bs1( bname );
	BlockSpecial bs2( bname );
	EXPECT_FALSE( bs1 < bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
}

TEST( BlockSpecialTest, OperatorLessThanEmptyObjectAndBlockDevice )
{
	std::string bname = get_block_name( 0 );
	SKIP_IF_BLOCK_DEVICE_DOESNT_EXIST(bname);

	// Test empty BlockSpecial object with name "" is before any block device.
	BlockSpecial::clear_cache();
	BlockSpecial bs1;
	BlockSpecial bs2( bname );
	EXPECT_TRUE( bs1 < bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
}

TEST( BlockSpecialTest, OperatorLessThanPlainFileAndBlockDevice )
{
	std::string bname = get_block_name( 0 );
	SKIP_IF_BLOCK_DEVICE_DOESNT_EXIST(bname);

	// Test one plain file BlockSpecial object is ordered before any block device.
	BlockSpecial::clear_cache();
	BlockSpecial bs1( "/" );
	BlockSpecial bs2( bname );
	EXPECT_TRUE( bs1 < bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
}

TEST( BlockSpecialTest, OperatorLessThanTwoDifferentBlockDevicesMajorNumbers )
{
	// Test one block device BlockSpecial object is ordered before another based on
	// major numbers.
	BlockSpecial::clear_cache();
	BlockSpecial::register_block_special( "/dummy_block2", 1, 1 );
	BlockSpecial::register_block_special( "/dummy_block1", 2, 0 );
	BlockSpecial bs1( "/dummy_block2" );
	BlockSpecial bs2( "/dummy_block1" );
	EXPECT_TRUE( bs1 < bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
}

TEST( BlockSpecialTest, OperatorLessThanTwoDifferentBlockDevicesMinorNumbers )
{
	// Test one block device BlockSpecial object is ordered before another based on
	// minor numbers.
	BlockSpecial::clear_cache();
	BlockSpecial::register_block_special( "/dummy_block2", 2, 0 );
	BlockSpecial::register_block_special( "/dummy_block1", 2, 1 );
	BlockSpecial bs1( "/dummy_block2" );
	BlockSpecial bs2( "/dummy_block1" );
	EXPECT_TRUE( bs1 < bs2 ) << ON_FAILURE_WHERE( bs1, bs2 );
}

}  // namespace GParted
