Skip to content

Commit b5c0a08

Browse files
ptrksSRombauts
authored andcommitted
Added SQLite header parsing functionality and associated tests (SRombauts#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
1 parent 54c7a18 commit b5c0a08

File tree

3 files changed

+233
-1
lines changed

3 files changed

+233
-1
lines changed

include/SQLiteCpp/Database.h

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
#include <SQLiteCpp/Column.h>
1414
#include <SQLiteCpp/Utils.h> // definition of nullptr for C++98/C++03 compilers
15-
1615
#include <string.h>
1716

1817
// Forward declarations to avoid inclusion of <sqlite3.h> in a header
@@ -53,6 +52,32 @@ const char* getLibVersion() noexcept; // nothrow
5352
/// Return SQLite version number using runtime call to the compiled library
5453
int getLibVersionNumber() noexcept; // nothrow
5554

55+
// Public structure for representing all fields contained within the SQLite header.
56+
// Official documentation for fields: https://www.sqlite.org/fileformat.html#the_database_header
57+
struct Header {
58+
unsigned char headerStr[16];
59+
unsigned int pageSizeBytes;
60+
unsigned char fileFormatWriteVersion;
61+
unsigned char fileFormatReadVersion;
62+
unsigned char reservedSpaceBytes;
63+
unsigned char maxEmbeddedPayloadFrac;
64+
unsigned char minEmbeddedPayloadFrac;
65+
unsigned char leafPayloadFrac;
66+
unsigned long fileChangeCounter;
67+
unsigned long databaseSizePages;
68+
unsigned long firstFreelistTrunkPage;
69+
unsigned long totalFreelistPages;
70+
unsigned long schemaCookie;
71+
unsigned long schemaFormatNumber;
72+
unsigned long defaultPageCacheSizeBytes;
73+
unsigned long largestBTreePageNumber;
74+
unsigned long databaseTextEncoding;
75+
unsigned long userVersion;
76+
unsigned long incrementalVaccumMode;
77+
unsigned long applicationId;
78+
unsigned long versionValidFor;
79+
unsigned long sqliteVersion;
80+
};
5681

5782
/**
5883
* @brief RAII management of a SQLite Database Connection.
@@ -434,6 +459,21 @@ class Database
434459
*/
435460
static bool isUnencrypted(const std::string& aFilename);
436461

462+
/**
463+
* @brief Parse SQLite header data from a database file.
464+
*
465+
* This function reads the first 100 bytes of a SQLite database file
466+
* and reconstructs groups of individual bytes into the associated fields
467+
* in a Header object.
468+
*
469+
* @param[in] aFilename path/uri to a file
470+
*
471+
* @return Header object containing file data
472+
*
473+
* @throw SQLite::Exception in case of error
474+
*/
475+
static Header getHeaderInfo(const std::string& aFilename);
476+
437477
/**
438478
* @brief BackupType for the backup() method
439479
*/

src/Database.cpp

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,136 @@ bool Database::isUnencrypted(const std::string& aFilename)
298298
return strncmp(header, "SQLite format 3\000", 16) == 0;
299299
}
300300

301+
// Parse header data from a database.
302+
Header Database::getHeaderInfo(const std::string& aFilename)
303+
{
304+
Header h;
305+
unsigned char buf[100];
306+
char* pBuf = reinterpret_cast<char*>(&buf[0]);
307+
char* pHeaderStr = reinterpret_cast<char*>(&h.headerStr[0]);
308+
309+
if (aFilename.empty())
310+
{
311+
throw SQLite::Exception("Could not open database, the aFilename parameter was empty.");
312+
}
313+
314+
std::ifstream fileBuffer(aFilename.c_str(), std::ios::in | std::ios::binary);
315+
316+
if (fileBuffer.is_open())
317+
{
318+
fileBuffer.seekg(0, std::ios::beg);
319+
fileBuffer.read(pBuf, 100);
320+
fileBuffer.close();
321+
strncpy(pHeaderStr, pBuf, 16);
322+
}
323+
324+
else
325+
{
326+
throw SQLite::Exception("Error opening file: " + aFilename);
327+
}
328+
329+
// If the "magic string" can't be found then header is invalid, corrupt or unreadable
330+
if (!strncmp(pHeaderStr, "SQLite format 3", 15) == 0)
331+
{
332+
throw SQLite::Exception("Invalid or encrypted SQLite header");
333+
}
334+
335+
h.pageSizeBytes = (buf[16] << 8) | buf[17];
336+
h.fileFormatWriteVersion = buf[18];
337+
h.fileFormatReadVersion = buf[19];
338+
h.reservedSpaceBytes = buf[20];
339+
h.maxEmbeddedPayloadFrac = buf[21];
340+
h.minEmbeddedPayloadFrac = buf[22];
341+
h.leafPayloadFrac = buf[23];
342+
343+
h.fileChangeCounter =
344+
(buf[24] << 24) |
345+
(buf[25] << 16) |
346+
(buf[26] << 8) |
347+
(buf[27] << 0);
348+
349+
h.databaseSizePages =
350+
(buf[28] << 24) |
351+
(buf[29] << 16) |
352+
(buf[30] << 8) |
353+
(buf[31] << 0);
354+
355+
h.firstFreelistTrunkPage =
356+
(buf[32] << 24) |
357+
(buf[33] << 16) |
358+
(buf[34] << 8) |
359+
(buf[35] << 0);
360+
361+
h.totalFreelistPages =
362+
(buf[36] << 24) |
363+
(buf[37] << 16) |
364+
(buf[38] << 8) |
365+
(buf[39] << 0);
366+
367+
h.schemaCookie =
368+
(buf[40] << 24) |
369+
(buf[41] << 16) |
370+
(buf[42] << 8) |
371+
(buf[43] << 0);
372+
373+
h.schemaFormatNumber =
374+
(buf[44] << 24) |
375+
(buf[45] << 16) |
376+
(buf[46] << 8) |
377+
(buf[47] << 0);
378+
379+
h.defaultPageCacheSizeBytes =
380+
(buf[48] << 24) |
381+
(buf[49] << 16) |
382+
(buf[50] << 8) |
383+
(buf[51] << 0);
384+
385+
h.largestBTreePageNumber =
386+
(buf[52] << 24) |
387+
(buf[53] << 16) |
388+
(buf[54] << 8) |
389+
(buf[55] << 0);
390+
391+
h.databaseTextEncoding =
392+
(buf[56] << 24) |
393+
(buf[57] << 16) |
394+
(buf[58] << 8) |
395+
(buf[59] << 0);
396+
397+
h.userVersion =
398+
(buf[60] << 24) |
399+
(buf[61] << 16) |
400+
(buf[62] << 8) |
401+
(buf[63] << 0);
402+
403+
h.incrementalVaccumMode =
404+
(buf[64] << 24) |
405+
(buf[65] << 16) |
406+
(buf[66] << 8) |
407+
(buf[67] << 0);
408+
409+
h.applicationId =
410+
(buf[68] << 24) |
411+
(buf[69] << 16) |
412+
(buf[70] << 8) |
413+
(buf[71] << 0);
414+
415+
h.versionValidFor =
416+
(buf[92] << 24) |
417+
(buf[93] << 16) |
418+
(buf[94] << 8) |
419+
(buf[95] << 0);
420+
421+
h.sqliteVersion =
422+
(buf[96] << 24) |
423+
(buf[97] << 16) |
424+
(buf[98] << 8) |
425+
(buf[99] << 0);
426+
427+
return h;
428+
}
429+
430+
301431
// This is a reference implementation of live backup taken from the official sit:
302432
// https://www.sqlite.org/backup.html
303433

tests/Database_test.cpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <gtest/gtest.h>
1717

1818
#include <cstdio>
19+
#include <fstream>
1920

2021
#ifdef SQLITECPP_ENABLE_ASSERT_HANDLER
2122
namespace SQLite
@@ -354,6 +355,67 @@ TEST(Database, loadExtension)
354355
// TODO: test a proper extension
355356
}
356357

358+
TEST(Database, getHeaderInfo)
359+
{
360+
remove("test.db3");
361+
{
362+
//Call without passing a database file name
363+
EXPECT_THROW(SQLite::Database::getHeaderInfo(""),SQLite::Exception);
364+
365+
//Call with a non existant database
366+
EXPECT_THROW(SQLite::Database::getHeaderInfo("test.db3"), SQLite::Exception);
367+
368+
//Simulate a corrupt header by writing garbage to a file
369+
unsigned char badData[100];
370+
char* pBadData = reinterpret_cast<char*>(&badData[0]);
371+
372+
std::ofstream corruptDb;
373+
corruptDb.open("corrupt.db3", std::ios::app | std::ios::binary);
374+
corruptDb.write(pBadData, 100);
375+
376+
EXPECT_THROW(SQLite::Database::getHeaderInfo("corrupt.db3"), SQLite::Exception);
377+
378+
remove("corrupt.db3");
379+
380+
// Create a new database
381+
SQLite::Database db("test.db3", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
382+
db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)");
383+
384+
// Set assorted SQLite header values using associated PRAGMA
385+
db.exec("PRAGMA main.user_version = 12345");
386+
db.exec("PRAGMA main.application_id = 2468");
387+
388+
// Parse header fields from test database
389+
SQLite::Header h = SQLite::Database::getHeaderInfo("test.db3");
390+
391+
//Test header values expliticly set via PRAGMA statements
392+
EXPECT_EQ(h.userVersion, 12345);
393+
EXPECT_EQ(h.applicationId, 2468);
394+
395+
//Test header values with expected default values
396+
EXPECT_EQ(h.pageSizeBytes, 4096);
397+
EXPECT_EQ(h.fileFormatWriteVersion,1);
398+
EXPECT_EQ(h.fileFormatReadVersion,1);
399+
EXPECT_EQ(h.reservedSpaceBytes,0);
400+
EXPECT_EQ(h.maxEmbeddedPayloadFrac, 64);
401+
EXPECT_EQ(h.minEmbeddedPayloadFrac, 32);
402+
EXPECT_EQ(h.leafPayloadFrac, 32);
403+
EXPECT_EQ(h.fileChangeCounter, 3);
404+
EXPECT_EQ(h.databaseSizePages, 2);
405+
EXPECT_EQ(h.firstFreelistTrunkPage, 0);
406+
EXPECT_EQ(h.totalFreelistPages, 0);
407+
EXPECT_EQ(h.schemaCookie, 1);
408+
EXPECT_EQ(h.schemaFormatNumber, 4);
409+
EXPECT_EQ(h.defaultPageCacheSizeBytes, 0);
410+
EXPECT_EQ(h.largestBTreePageNumber, 0);
411+
EXPECT_EQ(h.databaseTextEncoding, 1);
412+
EXPECT_EQ(h.incrementalVaccumMode, 0);
413+
EXPECT_EQ(h.versionValidFor, 3);
414+
EXPECT_EQ(h.sqliteVersion, SQLITE_VERSION_NUMBER);
415+
}
416+
remove("test.db3");
417+
}
418+
357419
#ifdef SQLITE_HAS_CODEC
358420
TEST(Database, encryptAndDecrypt)
359421
{

0 commit comments

Comments
 (0)