Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added SQLite header parsing functionality and associated tests #249

Merged
merged 6 commits into from
Dec 30, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion include/SQLiteCpp/Database.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

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

#include <cstdint>
#include <string.h>

// Forward declarations to avoid inclusion of <sqlite3.h> in a header
Expand Down Expand Up @@ -53,6 +53,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];
uint16_t pageSizeBytes;
unsigned char fileFormatWriteVersion;
unsigned char fileFormatReadVersion;
unsigned char reservedSpaceBytes;
unsigned char maxEmbeddedPayloadFrac;
unsigned char minEmbeddedPayloadFrac;
unsigned char leafPayloadFrac;
uint32_t fileChangeCounter;
uint32_t databaseSizePages;
uint32_t firstFreelistTrunkPage;
uint32_t totalFreelistPages;
uint32_t schemaCookie;
uint32_t schemaFormatNumber;
uint32_t defaultPageCacheSizeBytes;
uint32_t largestBTreePageNumber;
uint32_t databaseTextEncoding;
uint32_t userVersion;
uint32_t incrementalVaccumMode;
uint32_t applicationId;
uint32_t versionValidFor;
uint32_t sqliteVersion;
};

/**
* @brief RAII management of a SQLite Database Connection.
Expand Down Expand Up @@ -434,6 +460,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
132 changes: 132 additions & 0 deletions src/Database.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,138 @@ bool Database::isUnencrypted(const std::string& aFilename)
throw exception;
}

// Parse header data from a database.
Header Database::getHeaderInfo(const std::string& aFilename)
{
Header h;
unsigned char buf[100];

if (aFilename.length() > 0 )
{

std::ifstream fileBuffer(aFilename.c_str(), std::ios::in | std::ios::binary);

if (fileBuffer.is_open())
{
fileBuffer.seekg(0, std::ios::beg);
fileBuffer.read(reinterpret_cast<char*>(&buf[0]), 100);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to avoid these cast; perhaps have one additional "pointer of char" variable pointing to the buf.

unsigned char buf[100];
char* pBuf = reinterpret_cast<char*>(&buf[0]);

fileBuffer.close();
strncpy(reinterpret_cast<char*>(&h.headerStr[0]), reinterpret_cast<char*>(buf), 16);
}

else
{
const SQLite::Exception exception("Error opening file: " + aFilename);
throw exception;
}

// If the "magic string" can't be found then header is invalid, corrupt or unreadable
if (!strncmp(reinterpret_cast<const char*>(h.headerStr), "SQLite format 3", 15) == 0)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly here, headerStr could simply be a char[] variable directly on the header structure

{
const SQLite::Exception exception("Invalid SQLite header");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or encrypted database

throw exception;
}

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;
}

const SQLite::Exception exception("Could not open database, the aFilename parameter was empty.");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you perhaps put this exception at the start of the function like I did yesterday on a cleanup commit? It would avoid the return being inside the scope.

throw exception;
}


// This is a reference implementation of live backup taken from the official sit:
// https://www.sqlite.org/backup.html

Expand Down
43 changes: 43 additions & 0 deletions tests/Database_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,49 @@ TEST(Database, execException)
// TODO: test Database::createFunction()
// TODO: test Database::loadExtension()

TEST(Database, getHeaderInfo)
{
remove("test.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