Skip to content

Commit

Permalink
Added SQLite header parsing functionality and associated tests (#249)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ptrks authored and SRombauts committed Dec 30, 2019
1 parent 54c7a18 commit b5c0a08
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 1 deletion.
42 changes: 41 additions & 1 deletion include/SQLiteCpp/Database.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

#include <SQLiteCpp/Column.h>
#include <SQLiteCpp/Utils.h> // definition of nullptr for C++98/C++03 compilers

#include <string.h>

// Forward declarations to avoid inclusion of <sqlite3.h> in a header
Expand Down Expand Up @@ -53,6 +52,32 @@ const char* getLibVersion() noexcept; // nothrow
/// Return SQLite version number using runtime call to the compiled library
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.
Expand Down Expand Up @@ -434,6 +459,21 @@ class Database
*/
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
*/
Expand Down
130 changes: 130 additions & 0 deletions src/Database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,136 @@ bool Database::isUnencrypted(const std::string& aFilename)
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:
// https://www.sqlite.org/backup.html

Expand Down
62 changes: 62 additions & 0 deletions tests/Database_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <gtest/gtest.h>

#include <cstdio>
#include <fstream>

#ifdef SQLITECPP_ENABLE_ASSERT_HANDLER
namespace SQLite
Expand Down Expand Up @@ -354,6 +355,67 @@ TEST(Database, loadExtension)
// 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
TEST(Database, encryptAndDecrypt)
{
Expand Down

0 comments on commit b5c0a08

Please sign in to comment.