Added SQLite header parsing functionality and associated tests (#249)

* Added SQLite header parsing functionality and associated tests

* Removed unused header file.

* Removed an accidental copy pasted remove() statement

* Replaced stdint with plain old C types for now. Will apply fixed with datatypes to cpp11 branch

* Added test scenarios to simulate blank file name, non existant file and a corrupt header

* Refactored exception flow to match latest tidying, brought casts out of function calls and cleared up invalid header exception message
This commit is contained in:
Patrick Servello 2019-12-30 06:45:51 -06:00 committed by Sébastien Rombauts
parent 54c7a189af
commit b5c0a08d3d
3 changed files with 233 additions and 1 deletions

View File

@ -12,7 +12,6 @@
#include <SQLiteCpp/Column.h> #include <SQLiteCpp/Column.h>
#include <SQLiteCpp/Utils.h> // definition of nullptr for C++98/C++03 compilers #include <SQLiteCpp/Utils.h> // definition of nullptr for C++98/C++03 compilers
#include <string.h> #include <string.h>
// Forward declarations to avoid inclusion of <sqlite3.h> in a header // Forward declarations to avoid inclusion of <sqlite3.h> in a header
@ -53,6 +52,32 @@ const char* getLibVersion() noexcept; // nothrow
/// Return SQLite version number using runtime call to the compiled library /// Return SQLite version number using runtime call to the compiled library
int getLibVersionNumber() noexcept; // nothrow int getLibVersionNumber() noexcept; // nothrow
// Public structure for representing all fields contained within the SQLite header.
// Official documentation for fields: https://www.sqlite.org/fileformat.html#the_database_header
struct Header {
unsigned char headerStr[16];
unsigned int pageSizeBytes;
unsigned char fileFormatWriteVersion;
unsigned char fileFormatReadVersion;
unsigned char reservedSpaceBytes;
unsigned char maxEmbeddedPayloadFrac;
unsigned char minEmbeddedPayloadFrac;
unsigned char leafPayloadFrac;
unsigned long fileChangeCounter;
unsigned long databaseSizePages;
unsigned long firstFreelistTrunkPage;
unsigned long totalFreelistPages;
unsigned long schemaCookie;
unsigned long schemaFormatNumber;
unsigned long defaultPageCacheSizeBytes;
unsigned long largestBTreePageNumber;
unsigned long databaseTextEncoding;
unsigned long userVersion;
unsigned long incrementalVaccumMode;
unsigned long applicationId;
unsigned long versionValidFor;
unsigned long sqliteVersion;
};
/** /**
* @brief RAII management of a SQLite Database Connection. * @brief RAII management of a SQLite Database Connection.
@ -434,6 +459,21 @@ public:
*/ */
static bool isUnencrypted(const std::string& aFilename); static bool isUnencrypted(const std::string& aFilename);
/**
* @brief Parse SQLite header data from a database file.
*
* This function reads the first 100 bytes of a SQLite database file
* and reconstructs groups of individual bytes into the associated fields
* in a Header object.
*
* @param[in] aFilename path/uri to a file
*
* @return Header object containing file data
*
* @throw SQLite::Exception in case of error
*/
static Header getHeaderInfo(const std::string& aFilename);
/** /**
* @brief BackupType for the backup() method * @brief BackupType for the backup() method
*/ */

View File

@ -298,6 +298,136 @@ bool Database::isUnencrypted(const std::string& aFilename)
return strncmp(header, "SQLite format 3\000", 16) == 0; return strncmp(header, "SQLite format 3\000", 16) == 0;
} }
// Parse header data from a database.
Header Database::getHeaderInfo(const std::string& aFilename)
{
Header h;
unsigned char buf[100];
char* pBuf = reinterpret_cast<char*>(&buf[0]);
char* pHeaderStr = reinterpret_cast<char*>(&h.headerStr[0]);
if (aFilename.empty())
{
throw SQLite::Exception("Could not open database, the aFilename parameter was empty.");
}
std::ifstream fileBuffer(aFilename.c_str(), std::ios::in | std::ios::binary);
if (fileBuffer.is_open())
{
fileBuffer.seekg(0, std::ios::beg);
fileBuffer.read(pBuf, 100);
fileBuffer.close();
strncpy(pHeaderStr, pBuf, 16);
}
else
{
throw SQLite::Exception("Error opening file: " + aFilename);
}
// If the "magic string" can't be found then header is invalid, corrupt or unreadable
if (!strncmp(pHeaderStr, "SQLite format 3", 15) == 0)
{
throw SQLite::Exception("Invalid or encrypted SQLite header");
}
h.pageSizeBytes = (buf[16] << 8) | buf[17];
h.fileFormatWriteVersion = buf[18];
h.fileFormatReadVersion = buf[19];
h.reservedSpaceBytes = buf[20];
h.maxEmbeddedPayloadFrac = buf[21];
h.minEmbeddedPayloadFrac = buf[22];
h.leafPayloadFrac = buf[23];
h.fileChangeCounter =
(buf[24] << 24) |
(buf[25] << 16) |
(buf[26] << 8) |
(buf[27] << 0);
h.databaseSizePages =
(buf[28] << 24) |
(buf[29] << 16) |
(buf[30] << 8) |
(buf[31] << 0);
h.firstFreelistTrunkPage =
(buf[32] << 24) |
(buf[33] << 16) |
(buf[34] << 8) |
(buf[35] << 0);
h.totalFreelistPages =
(buf[36] << 24) |
(buf[37] << 16) |
(buf[38] << 8) |
(buf[39] << 0);
h.schemaCookie =
(buf[40] << 24) |
(buf[41] << 16) |
(buf[42] << 8) |
(buf[43] << 0);
h.schemaFormatNumber =
(buf[44] << 24) |
(buf[45] << 16) |
(buf[46] << 8) |
(buf[47] << 0);
h.defaultPageCacheSizeBytes =
(buf[48] << 24) |
(buf[49] << 16) |
(buf[50] << 8) |
(buf[51] << 0);
h.largestBTreePageNumber =
(buf[52] << 24) |
(buf[53] << 16) |
(buf[54] << 8) |
(buf[55] << 0);
h.databaseTextEncoding =
(buf[56] << 24) |
(buf[57] << 16) |
(buf[58] << 8) |
(buf[59] << 0);
h.userVersion =
(buf[60] << 24) |
(buf[61] << 16) |
(buf[62] << 8) |
(buf[63] << 0);
h.incrementalVaccumMode =
(buf[64] << 24) |
(buf[65] << 16) |
(buf[66] << 8) |
(buf[67] << 0);
h.applicationId =
(buf[68] << 24) |
(buf[69] << 16) |
(buf[70] << 8) |
(buf[71] << 0);
h.versionValidFor =
(buf[92] << 24) |
(buf[93] << 16) |
(buf[94] << 8) |
(buf[95] << 0);
h.sqliteVersion =
(buf[96] << 24) |
(buf[97] << 16) |
(buf[98] << 8) |
(buf[99] << 0);
return h;
}
// This is a reference implementation of live backup taken from the official sit: // This is a reference implementation of live backup taken from the official sit:
// https://www.sqlite.org/backup.html // https://www.sqlite.org/backup.html

View File

@ -16,6 +16,7 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <cstdio> #include <cstdio>
#include <fstream>
#ifdef SQLITECPP_ENABLE_ASSERT_HANDLER #ifdef SQLITECPP_ENABLE_ASSERT_HANDLER
namespace SQLite namespace SQLite
@ -354,6 +355,67 @@ TEST(Database, loadExtension)
// TODO: test a proper extension // TODO: test a proper extension
} }
TEST(Database, getHeaderInfo)
{
remove("test.db3");
{
//Call without passing a database file name
EXPECT_THROW(SQLite::Database::getHeaderInfo(""),SQLite::Exception);
//Call with a non existant database
EXPECT_THROW(SQLite::Database::getHeaderInfo("test.db3"), SQLite::Exception);
//Simulate a corrupt header by writing garbage to a file
unsigned char badData[100];
char* pBadData = reinterpret_cast<char*>(&badData[0]);
std::ofstream corruptDb;
corruptDb.open("corrupt.db3", std::ios::app | std::ios::binary);
corruptDb.write(pBadData, 100);
EXPECT_THROW(SQLite::Database::getHeaderInfo("corrupt.db3"), SQLite::Exception);
remove("corrupt.db3");
// Create a new database
SQLite::Database db("test.db3", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)");
// Set assorted SQLite header values using associated PRAGMA
db.exec("PRAGMA main.user_version = 12345");
db.exec("PRAGMA main.application_id = 2468");
// Parse header fields from test database
SQLite::Header h = SQLite::Database::getHeaderInfo("test.db3");
//Test header values expliticly set via PRAGMA statements
EXPECT_EQ(h.userVersion, 12345);
EXPECT_EQ(h.applicationId, 2468);
//Test header values with expected default values
EXPECT_EQ(h.pageSizeBytes, 4096);
EXPECT_EQ(h.fileFormatWriteVersion,1);
EXPECT_EQ(h.fileFormatReadVersion,1);
EXPECT_EQ(h.reservedSpaceBytes,0);
EXPECT_EQ(h.maxEmbeddedPayloadFrac, 64);
EXPECT_EQ(h.minEmbeddedPayloadFrac, 32);
EXPECT_EQ(h.leafPayloadFrac, 32);
EXPECT_EQ(h.fileChangeCounter, 3);
EXPECT_EQ(h.databaseSizePages, 2);
EXPECT_EQ(h.firstFreelistTrunkPage, 0);
EXPECT_EQ(h.totalFreelistPages, 0);
EXPECT_EQ(h.schemaCookie, 1);
EXPECT_EQ(h.schemaFormatNumber, 4);
EXPECT_EQ(h.defaultPageCacheSizeBytes, 0);
EXPECT_EQ(h.largestBTreePageNumber, 0);
EXPECT_EQ(h.databaseTextEncoding, 1);
EXPECT_EQ(h.incrementalVaccumMode, 0);
EXPECT_EQ(h.versionValidFor, 3);
EXPECT_EQ(h.sqliteVersion, SQLITE_VERSION_NUMBER);
}
remove("test.db3");
}
#ifdef SQLITE_HAS_CODEC #ifdef SQLITE_HAS_CODEC
TEST(Database, encryptAndDecrypt) TEST(Database, encryptAndDecrypt)
{ {