mirror of
https://github.com/cuberite/SQLiteCpp.git
synced 2025-08-04 09:46:02 -04:00
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:
parent
54c7a189af
commit
b5c0a08d3d
@ -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
|
||||
@ -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.
|
||||
@ -434,6 +459,21 @@ public:
|
||||
*/
|
||||
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
|
||||
*/
|
||||
|
130
src/Database.cpp
130
src/Database.cpp
@ -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
|
||||
|
||||
|
@ -16,6 +16,7 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
|
||||
#ifdef SQLITECPP_ENABLE_ASSERT_HANDLER
|
||||
namespace SQLite
|
||||
@ -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)
|
||||
{
|
||||
|
Loading…
x
Reference in New Issue
Block a user