summaryrefslogtreecommitdiffstats
path: root/ext/recover
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--ext/recover/dbdata.c942
-rw-r--r--ext/recover/recover1.test320
-rw-r--r--ext/recover/recover_common.tcl14
-rw-r--r--ext/recover/recoverclobber.test50
-rw-r--r--ext/recover/recovercorrupt.test67
-rw-r--r--ext/recover/recovercorrupt2.test289
-rw-r--r--ext/recover/recoverfault.test84
-rw-r--r--ext/recover/recoverfault2.test102
-rw-r--r--ext/recover/recoverold.test189
-rw-r--r--ext/recover/recoverpgsz.test100
-rw-r--r--ext/recover/recoverrowid.test50
-rw-r--r--ext/recover/recoverslowidx.test87
-rw-r--r--ext/recover/recoversql.test52
-rw-r--r--ext/recover/sqlite3recover.c2861
-rw-r--r--ext/recover/sqlite3recover.h249
-rw-r--r--ext/recover/test_recover.c311
16 files changed, 5767 insertions, 0 deletions
diff --git a/ext/recover/dbdata.c b/ext/recover/dbdata.c
new file mode 100644
index 0000000..9563ab5
--- /dev/null
+++ b/ext/recover/dbdata.c
@@ -0,0 +1,942 @@
+/*
+** 2019-04-17
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+******************************************************************************
+**
+** This file contains an implementation of two eponymous virtual tables,
+** "sqlite_dbdata" and "sqlite_dbptr". Both modules require that the
+** "sqlite_dbpage" eponymous virtual table be available.
+**
+** SQLITE_DBDATA:
+** sqlite_dbdata is used to extract data directly from a database b-tree
+** page and its associated overflow pages, bypassing the b-tree layer.
+** The table schema is equivalent to:
+**
+** CREATE TABLE sqlite_dbdata(
+** pgno INTEGER,
+** cell INTEGER,
+** field INTEGER,
+** value ANY,
+** schema TEXT HIDDEN
+** );
+**
+** IMPORTANT: THE VIRTUAL TABLE SCHEMA ABOVE IS SUBJECT TO CHANGE. IN THE
+** FUTURE NEW NON-HIDDEN COLUMNS MAY BE ADDED BETWEEN "value" AND
+** "schema".
+**
+** Each page of the database is inspected. If it cannot be interpreted as
+** a b-tree page, or if it is a b-tree page containing 0 entries, the
+** sqlite_dbdata table contains no rows for that page. Otherwise, the
+** table contains one row for each field in the record associated with
+** each cell on the page. For intkey b-trees, the key value is stored in
+** field -1.
+**
+** For example, for the database:
+**
+** CREATE TABLE t1(a, b); -- root page is page 2
+** INSERT INTO t1(rowid, a, b) VALUES(5, 'v', 'five');
+** INSERT INTO t1(rowid, a, b) VALUES(10, 'x', 'ten');
+**
+** the sqlite_dbdata table contains, as well as from entries related to
+** page 1, content equivalent to:
+**
+** INSERT INTO sqlite_dbdata(pgno, cell, field, value) VALUES
+** (2, 0, -1, 5 ),
+** (2, 0, 0, 'v' ),
+** (2, 0, 1, 'five'),
+** (2, 1, -1, 10 ),
+** (2, 1, 0, 'x' ),
+** (2, 1, 1, 'ten' );
+**
+** If database corruption is encountered, this module does not report an
+** error. Instead, it attempts to extract as much data as possible and
+** ignores the corruption.
+**
+** SQLITE_DBPTR:
+** The sqlite_dbptr table has the following schema:
+**
+** CREATE TABLE sqlite_dbptr(
+** pgno INTEGER,
+** child INTEGER,
+** schema TEXT HIDDEN
+** );
+**
+** It contains one entry for each b-tree pointer between a parent and
+** child page in the database.
+*/
+
+#if !defined(SQLITEINT_H)
+#include "sqlite3ext.h"
+
+typedef unsigned char u8;
+typedef unsigned int u32;
+
+#endif
+SQLITE_EXTENSION_INIT1
+#include <string.h>
+#include <assert.h>
+
+#ifndef SQLITE_OMIT_VIRTUALTABLE
+
+#define DBDATA_PADDING_BYTES 100
+
+typedef struct DbdataTable DbdataTable;
+typedef struct DbdataCursor DbdataCursor;
+
+/* Cursor object */
+struct DbdataCursor {
+ sqlite3_vtab_cursor base; /* Base class. Must be first */
+ sqlite3_stmt *pStmt; /* For fetching database pages */
+
+ int iPgno; /* Current page number */
+ u8 *aPage; /* Buffer containing page */
+ int nPage; /* Size of aPage[] in bytes */
+ int nCell; /* Number of cells on aPage[] */
+ int iCell; /* Current cell number */
+ int bOnePage; /* True to stop after one page */
+ int szDb;
+ sqlite3_int64 iRowid;
+
+ /* Only for the sqlite_dbdata table */
+ u8 *pRec; /* Buffer containing current record */
+ sqlite3_int64 nRec; /* Size of pRec[] in bytes */
+ sqlite3_int64 nHdr; /* Size of header in bytes */
+ int iField; /* Current field number */
+ u8 *pHdrPtr;
+ u8 *pPtr;
+ u32 enc; /* Text encoding */
+
+ sqlite3_int64 iIntkey; /* Integer key value */
+};
+
+/* Table object */
+struct DbdataTable {
+ sqlite3_vtab base; /* Base class. Must be first */
+ sqlite3 *db; /* The database connection */
+ sqlite3_stmt *pStmt; /* For fetching database pages */
+ int bPtr; /* True for sqlite3_dbptr table */
+};
+
+/* Column and schema definitions for sqlite_dbdata */
+#define DBDATA_COLUMN_PGNO 0
+#define DBDATA_COLUMN_CELL 1
+#define DBDATA_COLUMN_FIELD 2
+#define DBDATA_COLUMN_VALUE 3
+#define DBDATA_COLUMN_SCHEMA 4
+#define DBDATA_SCHEMA \
+ "CREATE TABLE x(" \
+ " pgno INTEGER," \
+ " cell INTEGER," \
+ " field INTEGER," \
+ " value ANY," \
+ " schema TEXT HIDDEN" \
+ ")"
+
+/* Column and schema definitions for sqlite_dbptr */
+#define DBPTR_COLUMN_PGNO 0
+#define DBPTR_COLUMN_CHILD 1
+#define DBPTR_COLUMN_SCHEMA 2
+#define DBPTR_SCHEMA \
+ "CREATE TABLE x(" \
+ " pgno INTEGER," \
+ " child INTEGER," \
+ " schema TEXT HIDDEN" \
+ ")"
+
+/*
+** Connect to an sqlite_dbdata (pAux==0) or sqlite_dbptr (pAux!=0) virtual
+** table.
+*/
+static int dbdataConnect(
+ sqlite3 *db,
+ void *pAux,
+ int argc, const char *const*argv,
+ sqlite3_vtab **ppVtab,
+ char **pzErr
+){
+ DbdataTable *pTab = 0;
+ int rc = sqlite3_declare_vtab(db, pAux ? DBPTR_SCHEMA : DBDATA_SCHEMA);
+
+ if( rc==SQLITE_OK ){
+ pTab = (DbdataTable*)sqlite3_malloc64(sizeof(DbdataTable));
+ if( pTab==0 ){
+ rc = SQLITE_NOMEM;
+ }else{
+ memset(pTab, 0, sizeof(DbdataTable));
+ pTab->db = db;
+ pTab->bPtr = (pAux!=0);
+ }
+ }
+
+ *ppVtab = (sqlite3_vtab*)pTab;
+ return rc;
+}
+
+/*
+** Disconnect from or destroy a sqlite_dbdata or sqlite_dbptr virtual table.
+*/
+static int dbdataDisconnect(sqlite3_vtab *pVtab){
+ DbdataTable *pTab = (DbdataTable*)pVtab;
+ if( pTab ){
+ sqlite3_finalize(pTab->pStmt);
+ sqlite3_free(pVtab);
+ }
+ return SQLITE_OK;
+}
+
+/*
+** This function interprets two types of constraints:
+**
+** schema=?
+** pgno=?
+**
+** If neither are present, idxNum is set to 0. If schema=? is present,
+** the 0x01 bit in idxNum is set. If pgno=? is present, the 0x02 bit
+** in idxNum is set.
+**
+** If both parameters are present, schema is in position 0 and pgno in
+** position 1.
+*/
+static int dbdataBestIndex(sqlite3_vtab *tab, sqlite3_index_info *pIdx){
+ DbdataTable *pTab = (DbdataTable*)tab;
+ int i;
+ int iSchema = -1;
+ int iPgno = -1;
+ int colSchema = (pTab->bPtr ? DBPTR_COLUMN_SCHEMA : DBDATA_COLUMN_SCHEMA);
+
+ for(i=0; i<pIdx->nConstraint; i++){
+ struct sqlite3_index_constraint *p = &pIdx->aConstraint[i];
+ if( p->op==SQLITE_INDEX_CONSTRAINT_EQ ){
+ if( p->iColumn==colSchema ){
+ if( p->usable==0 ) return SQLITE_CONSTRAINT;
+ iSchema = i;
+ }
+ if( p->iColumn==DBDATA_COLUMN_PGNO && p->usable ){
+ iPgno = i;
+ }
+ }
+ }
+
+ if( iSchema>=0 ){
+ pIdx->aConstraintUsage[iSchema].argvIndex = 1;
+ pIdx->aConstraintUsage[iSchema].omit = 1;
+ }
+ if( iPgno>=0 ){
+ pIdx->aConstraintUsage[iPgno].argvIndex = 1 + (iSchema>=0);
+ pIdx->aConstraintUsage[iPgno].omit = 1;
+ pIdx->estimatedCost = 100;
+ pIdx->estimatedRows = 50;
+
+ if( pTab->bPtr==0 && pIdx->nOrderBy && pIdx->aOrderBy[0].desc==0 ){
+ int iCol = pIdx->aOrderBy[0].iColumn;
+ if( pIdx->nOrderBy==1 ){
+ pIdx->orderByConsumed = (iCol==0 || iCol==1);
+ }else if( pIdx->nOrderBy==2 && pIdx->aOrderBy[1].desc==0 && iCol==0 ){
+ pIdx->orderByConsumed = (pIdx->aOrderBy[1].iColumn==1);
+ }
+ }
+
+ }else{
+ pIdx->estimatedCost = 100000000;
+ pIdx->estimatedRows = 1000000000;
+ }
+ pIdx->idxNum = (iSchema>=0 ? 0x01 : 0x00) | (iPgno>=0 ? 0x02 : 0x00);
+ return SQLITE_OK;
+}
+
+/*
+** Open a new sqlite_dbdata or sqlite_dbptr cursor.
+*/
+static int dbdataOpen(sqlite3_vtab *pVTab, sqlite3_vtab_cursor **ppCursor){
+ DbdataCursor *pCsr;
+
+ pCsr = (DbdataCursor*)sqlite3_malloc64(sizeof(DbdataCursor));
+ if( pCsr==0 ){
+ return SQLITE_NOMEM;
+ }else{
+ memset(pCsr, 0, sizeof(DbdataCursor));
+ pCsr->base.pVtab = pVTab;
+ }
+
+ *ppCursor = (sqlite3_vtab_cursor *)pCsr;
+ return SQLITE_OK;
+}
+
+/*
+** Restore a cursor object to the state it was in when first allocated
+** by dbdataOpen().
+*/
+static void dbdataResetCursor(DbdataCursor *pCsr){
+ DbdataTable *pTab = (DbdataTable*)(pCsr->base.pVtab);
+ if( pTab->pStmt==0 ){
+ pTab->pStmt = pCsr->pStmt;
+ }else{
+ sqlite3_finalize(pCsr->pStmt);
+ }
+ pCsr->pStmt = 0;
+ pCsr->iPgno = 1;
+ pCsr->iCell = 0;
+ pCsr->iField = 0;
+ pCsr->bOnePage = 0;
+ sqlite3_free(pCsr->aPage);
+ sqlite3_free(pCsr->pRec);
+ pCsr->pRec = 0;
+ pCsr->aPage = 0;
+}
+
+/*
+** Close an sqlite_dbdata or sqlite_dbptr cursor.
+*/
+static int dbdataClose(sqlite3_vtab_cursor *pCursor){
+ DbdataCursor *pCsr = (DbdataCursor*)pCursor;
+ dbdataResetCursor(pCsr);
+ sqlite3_free(pCsr);
+ return SQLITE_OK;
+}
+
+/*
+** Utility methods to decode 16 and 32-bit big-endian unsigned integers.
+*/
+static u32 get_uint16(unsigned char *a){
+ return (a[0]<<8)|a[1];
+}
+static u32 get_uint32(unsigned char *a){
+ return ((u32)a[0]<<24)
+ | ((u32)a[1]<<16)
+ | ((u32)a[2]<<8)
+ | ((u32)a[3]);
+}
+
+/*
+** Load page pgno from the database via the sqlite_dbpage virtual table.
+** If successful, set (*ppPage) to point to a buffer containing the page
+** data, (*pnPage) to the size of that buffer in bytes and return
+** SQLITE_OK. In this case it is the responsibility of the caller to
+** eventually free the buffer using sqlite3_free().
+**
+** Or, if an error occurs, set both (*ppPage) and (*pnPage) to 0 and
+** return an SQLite error code.
+*/
+static int dbdataLoadPage(
+ DbdataCursor *pCsr, /* Cursor object */
+ u32 pgno, /* Page number of page to load */
+ u8 **ppPage, /* OUT: pointer to page buffer */
+ int *pnPage /* OUT: Size of (*ppPage) in bytes */
+){
+ int rc2;
+ int rc = SQLITE_OK;
+ sqlite3_stmt *pStmt = pCsr->pStmt;
+
+ *ppPage = 0;
+ *pnPage = 0;
+ if( pgno>0 ){
+ sqlite3_bind_int64(pStmt, 2, pgno);
+ if( SQLITE_ROW==sqlite3_step(pStmt) ){
+ int nCopy = sqlite3_column_bytes(pStmt, 0);
+ if( nCopy>0 ){
+ u8 *pPage;
+ pPage = (u8*)sqlite3_malloc64(nCopy + DBDATA_PADDING_BYTES);
+ if( pPage==0 ){
+ rc = SQLITE_NOMEM;
+ }else{
+ const u8 *pCopy = sqlite3_column_blob(pStmt, 0);
+ memcpy(pPage, pCopy, nCopy);
+ memset(&pPage[nCopy], 0, DBDATA_PADDING_BYTES);
+ }
+ *ppPage = pPage;
+ *pnPage = nCopy;
+ }
+ }
+ rc2 = sqlite3_reset(pStmt);
+ if( rc==SQLITE_OK ) rc = rc2;
+ }
+
+ return rc;
+}
+
+/*
+** Read a varint. Put the value in *pVal and return the number of bytes.
+*/
+static int dbdataGetVarint(const u8 *z, sqlite3_int64 *pVal){
+ sqlite3_uint64 u = 0;
+ int i;
+ for(i=0; i<8; i++){
+ u = (u<<7) + (z[i]&0x7f);
+ if( (z[i]&0x80)==0 ){ *pVal = (sqlite3_int64)u; return i+1; }
+ }
+ u = (u<<8) + (z[i]&0xff);
+ *pVal = (sqlite3_int64)u;
+ return 9;
+}
+
+/*
+** Like dbdataGetVarint(), but set the output to 0 if it is less than 0
+** or greater than 0xFFFFFFFF. This can be used for all varints in an
+** SQLite database except for key values in intkey tables.
+*/
+static int dbdataGetVarintU32(const u8 *z, sqlite3_int64 *pVal){
+ sqlite3_int64 val;
+ int nRet = dbdataGetVarint(z, &val);
+ if( val<0 || val>0xFFFFFFFF ) val = 0;
+ *pVal = val;
+ return nRet;
+}
+
+/*
+** Return the number of bytes of space used by an SQLite value of type
+** eType.
+*/
+static int dbdataValueBytes(int eType){
+ switch( eType ){
+ case 0: case 8: case 9:
+ case 10: case 11:
+ return 0;
+ case 1:
+ return 1;
+ case 2:
+ return 2;
+ case 3:
+ return 3;
+ case 4:
+ return 4;
+ case 5:
+ return 6;
+ case 6:
+ case 7:
+ return 8;
+ default:
+ if( eType>0 ){
+ return ((eType-12) / 2);
+ }
+ return 0;
+ }
+}
+
+/*
+** Load a value of type eType from buffer pData and use it to set the
+** result of context object pCtx.
+*/
+static void dbdataValue(
+ sqlite3_context *pCtx,
+ u32 enc,
+ int eType,
+ u8 *pData,
+ sqlite3_int64 nData
+){
+ if( eType>=0 && dbdataValueBytes(eType)<=nData ){
+ switch( eType ){
+ case 0:
+ case 10:
+ case 11:
+ sqlite3_result_null(pCtx);
+ break;
+
+ case 8:
+ sqlite3_result_int(pCtx, 0);
+ break;
+ case 9:
+ sqlite3_result_int(pCtx, 1);
+ break;
+
+ case 1: case 2: case 3: case 4: case 5: case 6: case 7: {
+ sqlite3_uint64 v = (signed char)pData[0];
+ pData++;
+ switch( eType ){
+ case 7:
+ case 6: v = (v<<16) + (pData[0]<<8) + pData[1]; pData += 2;
+ case 5: v = (v<<16) + (pData[0]<<8) + pData[1]; pData += 2;
+ case 4: v = (v<<8) + pData[0]; pData++;
+ case 3: v = (v<<8) + pData[0]; pData++;
+ case 2: v = (v<<8) + pData[0]; pData++;
+ }
+
+ if( eType==7 ){
+ double r;
+ memcpy(&r, &v, sizeof(r));
+ sqlite3_result_double(pCtx, r);
+ }else{
+ sqlite3_result_int64(pCtx, (sqlite3_int64)v);
+ }
+ break;
+ }
+
+ default: {
+ int n = ((eType-12) / 2);
+ if( eType % 2 ){
+ switch( enc ){
+#ifndef SQLITE_OMIT_UTF16
+ case SQLITE_UTF16BE:
+ sqlite3_result_text16be(pCtx, (void*)pData, n, SQLITE_TRANSIENT);
+ break;
+ case SQLITE_UTF16LE:
+ sqlite3_result_text16le(pCtx, (void*)pData, n, SQLITE_TRANSIENT);
+ break;
+#endif
+ default:
+ sqlite3_result_text(pCtx, (char*)pData, n, SQLITE_TRANSIENT);
+ break;
+ }
+ }else{
+ sqlite3_result_blob(pCtx, pData, n, SQLITE_TRANSIENT);
+ }
+ }
+ }
+ }
+}
+
+/*
+** Move an sqlite_dbdata or sqlite_dbptr cursor to the next entry.
+*/
+static int dbdataNext(sqlite3_vtab_cursor *pCursor){
+ DbdataCursor *pCsr = (DbdataCursor*)pCursor;
+ DbdataTable *pTab = (DbdataTable*)pCursor->pVtab;
+
+ pCsr->iRowid++;
+ while( 1 ){
+ int rc;
+ int iOff = (pCsr->iPgno==1 ? 100 : 0);
+ int bNextPage = 0;
+
+ if( pCsr->aPage==0 ){
+ while( 1 ){
+ if( pCsr->bOnePage==0 && pCsr->iPgno>pCsr->szDb ) return SQLITE_OK;
+ rc = dbdataLoadPage(pCsr, pCsr->iPgno, &pCsr->aPage, &pCsr->nPage);
+ if( rc!=SQLITE_OK ) return rc;
+ if( pCsr->aPage ) break;
+ if( pCsr->bOnePage ) return SQLITE_OK;
+ pCsr->iPgno++;
+ }
+ pCsr->iCell = pTab->bPtr ? -2 : 0;
+ pCsr->nCell = get_uint16(&pCsr->aPage[iOff+3]);
+ }
+
+ if( pTab->bPtr ){
+ if( pCsr->aPage[iOff]!=0x02 && pCsr->aPage[iOff]!=0x05 ){
+ pCsr->iCell = pCsr->nCell;
+ }
+ pCsr->iCell++;
+ if( pCsr->iCell>=pCsr->nCell ){
+ sqlite3_free(pCsr->aPage);
+ pCsr->aPage = 0;
+ if( pCsr->bOnePage ) return SQLITE_OK;
+ pCsr->iPgno++;
+ }else{
+ return SQLITE_OK;
+ }
+ }else{
+ /* If there is no record loaded, load it now. */
+ if( pCsr->pRec==0 ){
+ int bHasRowid = 0;
+ int nPointer = 0;
+ sqlite3_int64 nPayload = 0;
+ sqlite3_int64 nHdr = 0;
+ int iHdr;
+ int U, X;
+ int nLocal;
+
+ switch( pCsr->aPage[iOff] ){
+ case 0x02:
+ nPointer = 4;
+ break;
+ case 0x0a:
+ break;
+ case 0x0d:
+ bHasRowid = 1;
+ break;
+ default:
+ /* This is not a b-tree page with records on it. Continue. */
+ pCsr->iCell = pCsr->nCell;
+ break;
+ }
+
+ if( pCsr->iCell>=pCsr->nCell ){
+ bNextPage = 1;
+ }else{
+
+ iOff += 8 + nPointer + pCsr->iCell*2;
+ if( iOff>pCsr->nPage ){
+ bNextPage = 1;
+ }else{
+ iOff = get_uint16(&pCsr->aPage[iOff]);
+ }
+
+ /* For an interior node cell, skip past the child-page number */
+ iOff += nPointer;
+
+ /* Load the "byte of payload including overflow" field */
+ if( bNextPage || iOff>pCsr->nPage ){
+ bNextPage = 1;
+ }else{
+ iOff += dbdataGetVarintU32(&pCsr->aPage[iOff], &nPayload);
+ }
+
+ /* If this is a leaf intkey cell, load the rowid */
+ if( bHasRowid && !bNextPage && iOff<pCsr->nPage ){
+ iOff += dbdataGetVarint(&pCsr->aPage[iOff], &pCsr->iIntkey);
+ }
+
+ /* Figure out how much data to read from the local page */
+ U = pCsr->nPage;
+ if( bHasRowid ){
+ X = U-35;
+ }else{
+ X = ((U-12)*64/255)-23;
+ }
+ if( nPayload<=X ){
+ nLocal = nPayload;
+ }else{
+ int M, K;
+ M = ((U-12)*32/255)-23;
+ K = M+((nPayload-M)%(U-4));
+ if( K<=X ){
+ nLocal = K;
+ }else{
+ nLocal = M;
+ }
+ }
+
+ if( bNextPage || nLocal+iOff>pCsr->nPage ){
+ bNextPage = 1;
+ }else{
+
+ /* Allocate space for payload. And a bit more to catch small buffer
+ ** overruns caused by attempting to read a varint or similar from
+ ** near the end of a corrupt record. */
+ pCsr->pRec = (u8*)sqlite3_malloc64(nPayload+DBDATA_PADDING_BYTES);
+ if( pCsr->pRec==0 ) return SQLITE_NOMEM;
+ memset(pCsr->pRec, 0, nPayload+DBDATA_PADDING_BYTES);
+ pCsr->nRec = nPayload;
+
+ /* Load the nLocal bytes of payload */
+ memcpy(pCsr->pRec, &pCsr->aPage[iOff], nLocal);
+ iOff += nLocal;
+
+ /* Load content from overflow pages */
+ if( nPayload>nLocal ){
+ sqlite3_int64 nRem = nPayload - nLocal;
+ u32 pgnoOvfl = get_uint32(&pCsr->aPage[iOff]);
+ while( nRem>0 ){
+ u8 *aOvfl = 0;
+ int nOvfl = 0;
+ int nCopy;
+ rc = dbdataLoadPage(pCsr, pgnoOvfl, &aOvfl, &nOvfl);
+ assert( rc!=SQLITE_OK || aOvfl==0 || nOvfl==pCsr->nPage );
+ if( rc!=SQLITE_OK ) return rc;
+ if( aOvfl==0 ) break;
+
+ nCopy = U-4;
+ if( nCopy>nRem ) nCopy = nRem;
+ memcpy(&pCsr->pRec[nPayload-nRem], &aOvfl[4], nCopy);
+ nRem -= nCopy;
+
+ pgnoOvfl = get_uint32(aOvfl);
+ sqlite3_free(aOvfl);
+ }
+ }
+
+ iHdr = dbdataGetVarintU32(pCsr->pRec, &nHdr);
+ if( nHdr>nPayload ) nHdr = 0;
+ pCsr->nHdr = nHdr;
+ pCsr->pHdrPtr = &pCsr->pRec[iHdr];
+ pCsr->pPtr = &pCsr->pRec[pCsr->nHdr];
+ pCsr->iField = (bHasRowid ? -1 : 0);
+ }
+ }
+ }else{
+ pCsr->iField++;
+ if( pCsr->iField>0 ){
+ sqlite3_int64 iType;
+ if( pCsr->pHdrPtr>&pCsr->pRec[pCsr->nRec] ){
+ bNextPage = 1;
+ }else{
+ pCsr->pHdrPtr += dbdataGetVarintU32(pCsr->pHdrPtr, &iType);
+ pCsr->pPtr += dbdataValueBytes(iType);
+ }
+ }
+ }
+
+ if( bNextPage ){
+ sqlite3_free(pCsr->aPage);
+ sqlite3_free(pCsr->pRec);
+ pCsr->aPage = 0;
+ pCsr->pRec = 0;
+ if( pCsr->bOnePage ) return SQLITE_OK;
+ pCsr->iPgno++;
+ }else{
+ if( pCsr->iField<0 || pCsr->pHdrPtr<&pCsr->pRec[pCsr->nHdr] ){
+ return SQLITE_OK;
+ }
+
+ /* Advance to the next cell. The next iteration of the loop will load
+ ** the record and so on. */
+ sqlite3_free(pCsr->pRec);
+ pCsr->pRec = 0;
+ pCsr->iCell++;
+ }
+ }
+ }
+
+ assert( !"can't get here" );
+ return SQLITE_OK;
+}
+
+/*
+** Return true if the cursor is at EOF.
+*/
+static int dbdataEof(sqlite3_vtab_cursor *pCursor){
+ DbdataCursor *pCsr = (DbdataCursor*)pCursor;
+ return pCsr->aPage==0;
+}
+
+/*
+** Return true if nul-terminated string zSchema ends in "()". Or false
+** otherwise.
+*/
+static int dbdataIsFunction(const char *zSchema){
+ size_t n = strlen(zSchema);
+ if( n>2 && zSchema[n-2]=='(' && zSchema[n-1]==')' ){
+ return (int)n-2;
+ }
+ return 0;
+}
+
+/*
+** Determine the size in pages of database zSchema (where zSchema is
+** "main", "temp" or the name of an attached database) and set
+** pCsr->szDb accordingly. If successful, return SQLITE_OK. Otherwise,
+** an SQLite error code.
+*/
+static int dbdataDbsize(DbdataCursor *pCsr, const char *zSchema){
+ DbdataTable *pTab = (DbdataTable*)pCsr->base.pVtab;
+ char *zSql = 0;
+ int rc, rc2;
+ int nFunc = 0;
+ sqlite3_stmt *pStmt = 0;
+
+ if( (nFunc = dbdataIsFunction(zSchema))>0 ){
+ zSql = sqlite3_mprintf("SELECT %.*s(0)", nFunc, zSchema);
+ }else{
+ zSql = sqlite3_mprintf("PRAGMA %Q.page_count", zSchema);
+ }
+ if( zSql==0 ) return SQLITE_NOMEM;
+
+ rc = sqlite3_prepare_v2(pTab->db, zSql, -1, &pStmt, 0);
+ sqlite3_free(zSql);
+ if( rc==SQLITE_OK && sqlite3_step(pStmt)==SQLITE_ROW ){
+ pCsr->szDb = sqlite3_column_int(pStmt, 0);
+ }
+ rc2 = sqlite3_finalize(pStmt);
+ if( rc==SQLITE_OK ) rc = rc2;
+ return rc;
+}
+
+/*
+** Attempt to figure out the encoding of the database by retrieving page 1
+** and inspecting the header field. If successful, set the pCsr->enc variable
+** and return SQLITE_OK. Otherwise, return an SQLite error code.
+*/
+static int dbdataGetEncoding(DbdataCursor *pCsr){
+ int rc = SQLITE_OK;
+ int nPg1 = 0;
+ u8 *aPg1 = 0;
+ rc = dbdataLoadPage(pCsr, 1, &aPg1, &nPg1);
+ assert( rc!=SQLITE_OK || nPg1==0 || nPg1>=512 );
+ if( rc==SQLITE_OK && nPg1>0 ){
+ pCsr->enc = get_uint32(&aPg1[56]);
+ }
+ sqlite3_free(aPg1);
+ return rc;
+}
+
+
+/*
+** xFilter method for sqlite_dbdata and sqlite_dbptr.
+*/
+static int dbdataFilter(
+ sqlite3_vtab_cursor *pCursor,
+ int idxNum, const char *idxStr,
+ int argc, sqlite3_value **argv
+){
+ DbdataCursor *pCsr = (DbdataCursor*)pCursor;
+ DbdataTable *pTab = (DbdataTable*)pCursor->pVtab;
+ int rc = SQLITE_OK;
+ const char *zSchema = "main";
+
+ dbdataResetCursor(pCsr);
+ assert( pCsr->iPgno==1 );
+ if( idxNum & 0x01 ){
+ zSchema = (const char*)sqlite3_value_text(argv[0]);
+ if( zSchema==0 ) zSchema = "";
+ }
+ if( idxNum & 0x02 ){
+ pCsr->iPgno = sqlite3_value_int(argv[(idxNum & 0x01)]);
+ pCsr->bOnePage = 1;
+ }else{
+ rc = dbdataDbsize(pCsr, zSchema);
+ }
+
+ if( rc==SQLITE_OK ){
+ int nFunc = 0;
+ if( pTab->pStmt ){
+ pCsr->pStmt = pTab->pStmt;
+ pTab->pStmt = 0;
+ }else if( (nFunc = dbdataIsFunction(zSchema))>0 ){
+ char *zSql = sqlite3_mprintf("SELECT %.*s(?2)", nFunc, zSchema);
+ if( zSql==0 ){
+ rc = SQLITE_NOMEM;
+ }else{
+ rc = sqlite3_prepare_v2(pTab->db, zSql, -1, &pCsr->pStmt, 0);
+ sqlite3_free(zSql);
+ }
+ }else{
+ rc = sqlite3_prepare_v2(pTab->db,
+ "SELECT data FROM sqlite_dbpage(?) WHERE pgno=?", -1,
+ &pCsr->pStmt, 0
+ );
+ }
+ }
+ if( rc==SQLITE_OK ){
+ rc = sqlite3_bind_text(pCsr->pStmt, 1, zSchema, -1, SQLITE_TRANSIENT);
+ }else{
+ pTab->base.zErrMsg = sqlite3_mprintf("%s", sqlite3_errmsg(pTab->db));
+ }
+
+ /* Try to determine the encoding of the db by inspecting the header
+ ** field on page 1. */
+ if( rc==SQLITE_OK ){
+ rc = dbdataGetEncoding(pCsr);
+ }
+
+ if( rc==SQLITE_OK ){
+ rc = dbdataNext(pCursor);
+ }
+ return rc;
+}
+
+/*
+** Return a column for the sqlite_dbdata or sqlite_dbptr table.
+*/
+static int dbdataColumn(
+ sqlite3_vtab_cursor *pCursor,
+ sqlite3_context *ctx,
+ int i
+){
+ DbdataCursor *pCsr = (DbdataCursor*)pCursor;
+ DbdataTable *pTab = (DbdataTable*)pCursor->pVtab;
+ if( pTab->bPtr ){
+ switch( i ){
+ case DBPTR_COLUMN_PGNO:
+ sqlite3_result_int64(ctx, pCsr->iPgno);
+ break;
+ case DBPTR_COLUMN_CHILD: {
+ int iOff = pCsr->iPgno==1 ? 100 : 0;
+ if( pCsr->iCell<0 ){
+ iOff += 8;
+ }else{
+ iOff += 12 + pCsr->iCell*2;
+ if( iOff>pCsr->nPage ) return SQLITE_OK;
+ iOff = get_uint16(&pCsr->aPage[iOff]);
+ }
+ if( iOff<=pCsr->nPage ){
+ sqlite3_result_int64(ctx, get_uint32(&pCsr->aPage[iOff]));
+ }
+ break;
+ }
+ }
+ }else{
+ switch( i ){
+ case DBDATA_COLUMN_PGNO:
+ sqlite3_result_int64(ctx, pCsr->iPgno);
+ break;
+ case DBDATA_COLUMN_CELL:
+ sqlite3_result_int(ctx, pCsr->iCell);
+ break;
+ case DBDATA_COLUMN_FIELD:
+ sqlite3_result_int(ctx, pCsr->iField);
+ break;
+ case DBDATA_COLUMN_VALUE: {
+ if( pCsr->iField<0 ){
+ sqlite3_result_int64(ctx, pCsr->iIntkey);
+ }else if( &pCsr->pRec[pCsr->nRec] >= pCsr->pPtr ){
+ sqlite3_int64 iType;
+ dbdataGetVarintU32(pCsr->pHdrPtr, &iType);
+ dbdataValue(
+ ctx, pCsr->enc, iType, pCsr->pPtr,
+ &pCsr->pRec[pCsr->nRec] - pCsr->pPtr
+ );
+ }
+ break;
+ }
+ }
+ }
+ return SQLITE_OK;
+}
+
+/*
+** Return the rowid for an sqlite_dbdata or sqlite_dptr table.
+*/
+static int dbdataRowid(sqlite3_vtab_cursor *pCursor, sqlite_int64 *pRowid){
+ DbdataCursor *pCsr = (DbdataCursor*)pCursor;
+ *pRowid = pCsr->iRowid;
+ return SQLITE_OK;
+}
+
+
+/*
+** Invoke this routine to register the "sqlite_dbdata" virtual table module
+*/
+static int sqlite3DbdataRegister(sqlite3 *db){
+ static sqlite3_module dbdata_module = {
+ 0, /* iVersion */
+ 0, /* xCreate */
+ dbdataConnect, /* xConnect */
+ dbdataBestIndex, /* xBestIndex */
+ dbdataDisconnect, /* xDisconnect */
+ 0, /* xDestroy */
+ dbdataOpen, /* xOpen - open a cursor */
+ dbdataClose, /* xClose - close a cursor */
+ dbdataFilter, /* xFilter - configure scan constraints */
+ dbdataNext, /* xNext - advance a cursor */
+ dbdataEof, /* xEof - check for end of scan */
+ dbdataColumn, /* xColumn - read data */
+ dbdataRowid, /* xRowid - read data */
+ 0, /* xUpdate */
+ 0, /* xBegin */
+ 0, /* xSync */
+ 0, /* xCommit */
+ 0, /* xRollback */
+ 0, /* xFindMethod */
+ 0, /* xRename */
+ 0, /* xSavepoint */
+ 0, /* xRelease */
+ 0, /* xRollbackTo */
+ 0 /* xShadowName */
+ };
+
+ int rc = sqlite3_create_module(db, "sqlite_dbdata", &dbdata_module, 0);
+ if( rc==SQLITE_OK ){
+ rc = sqlite3_create_module(db, "sqlite_dbptr", &dbdata_module, (void*)1);
+ }
+ return rc;
+}
+
+#ifdef _WIN32
+__declspec(dllexport)
+#endif
+int sqlite3_dbdata_init(
+ sqlite3 *db,
+ char **pzErrMsg,
+ const sqlite3_api_routines *pApi
+){
+ SQLITE_EXTENSION_INIT2(pApi);
+ return sqlite3DbdataRegister(db);
+}
+
+#endif /* ifndef SQLITE_OMIT_VIRTUALTABLE */
diff --git a/ext/recover/recover1.test b/ext/recover/recover1.test
new file mode 100644
index 0000000..dba96f5
--- /dev/null
+++ b/ext/recover/recover1.test
@@ -0,0 +1,320 @@
+# 2022 August 28
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+
+source [file join [file dirname [info script]] recover_common.tcl]
+set testprefix recover1
+
+proc compare_result {db1 db2 sql} {
+ set r1 [$db1 eval $sql]
+ set r2 [$db2 eval $sql]
+ if {$r1 != $r2} {
+ puts "r1: $r1"
+ puts "r2: $r2"
+ error "mismatch for $sql"
+ }
+ return ""
+}
+
+proc compare_dbs {db1 db2} {
+ compare_result $db1 $db2 "SELECT sql FROM sqlite_master ORDER BY 1"
+ foreach tbl [$db1 eval {SELECT name FROM sqlite_master WHERE type='table'}] {
+ compare_result $db1 $db2 "SELECT * FROM $tbl"
+ }
+
+ compare_result $db1 $db2 "PRAGMA page_size"
+ compare_result $db1 $db2 "PRAGMA auto_vacuum"
+ compare_result $db1 $db2 "PRAGMA encoding"
+ compare_result $db1 $db2 "PRAGMA user_version"
+ compare_result $db1 $db2 "PRAGMA application_id"
+}
+
+proc do_recover_test {tn} {
+ forcedelete test.db2
+ forcedelete rstate.db
+
+ uplevel [list do_test $tn.1 {
+ set R [sqlite3_recover_init db main test.db2]
+ $R config testdb rstate.db
+ $R run
+ $R finish
+ } {}]
+
+ sqlite3 db2 test.db2
+ uplevel [list do_test $tn.2 [list compare_dbs db db2] {}]
+ db2 close
+
+ forcedelete test.db2
+ forcedelete rstate.db
+
+ uplevel [list do_test $tn.3 {
+ set ::sqlhook [list]
+ set R [sqlite3_recover_init_sql db main my_sql_hook]
+ $R config testdb rstate.db
+ $R config rowids 1
+ $R run
+ $R finish
+ } {}]
+
+ sqlite3 db2 test.db2
+ execsql [join $::sqlhook ";"] db2
+ db2 close
+ sqlite3 db2 test.db2
+ uplevel [list do_test $tn.4 [list compare_dbs db db2] {}]
+ db2 close
+}
+
+proc my_sql_hook {sql} {
+ lappend ::sqlhook $sql
+ return 0
+}
+
+do_execsql_test 1.0 {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b);
+ CREATE TABLE t2(a INTEGER PRIMARY KEY, b) WITHOUT ROWID;
+ WITH s(i) AS (
+ SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<10
+ )
+ INSERT INTO t1 SELECT i*2, hex(randomblob(250)) FROM s;
+ INSERT INTO t2 SELECT * FROM t1;
+}
+
+do_recover_test 1
+
+do_execsql_test 2.0 {
+ ALTER TABLE t1 ADD COLUMN c DEFAULT 'xyz'
+}
+do_recover_test 2
+
+do_execsql_test 3.0 {
+ CREATE INDEX i1 ON t1(c);
+}
+do_recover_test 3
+
+do_execsql_test 4.0 {
+ CREATE VIEW v1 AS SELECT * FROM t2;
+}
+do_recover_test 4
+
+do_execsql_test 5.0 {
+ CREATE UNIQUE INDEX i2 ON t1(c, b);
+}
+do_recover_test 5
+
+#--------------------------------------------------------------------------
+#
+reset_db
+do_execsql_test 6.0 {
+ CREATE TABLE t1(
+ a INTEGER PRIMARY KEY,
+ b INT,
+ c TEXT,
+ d INT GENERATED ALWAYS AS (a*abs(b)) VIRTUAL,
+ e TEXT GENERATED ALWAYS AS (substr(c,b,b+1)) STORED,
+ f TEXT GENERATED ALWAYS AS (substr(c,b,b+1)) STORED
+ );
+
+ INSERT INTO t1 VALUES(1, 2, 'hello world');
+}
+do_recover_test 6
+
+do_execsql_test 7.0 {
+ CREATE TABLE t2(i, j GENERATED ALWAYS AS (i+1) STORED, k);
+ INSERT INTO t2 VALUES(10, 'ten');
+}
+do_execsql_test 7.1 {
+ SELECT * FROM t2
+} {10 11 ten}
+
+do_recover_test 7.2
+
+#--------------------------------------------------------------------------
+#
+reset_db
+do_execsql_test 8.0 {
+ CREATE TABLE x1(a INTEGER PRIMARY KEY AUTOINCREMENT, b, c);
+ WITH s(i) AS (
+ SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<2
+ )
+ INSERT INTO x1(b, c) SELECT hex(randomblob(100)), hex(randomblob(100)) FROM s;
+
+ CREATE INDEX x1b ON x1(b);
+ CREATE INDEX x1cb ON x1(c, b);
+ DELETE FROM x1 WHERE a>50;
+
+ ANALYZE;
+}
+
+do_recover_test 8
+
+#-------------------------------------------------------------------------
+reset_db
+ifcapable fts5 {
+ do_execsql_test 9.1 {
+ CREATE VIRTUAL TABLE ft5 USING fts5(a, b);
+ INSERT INTO ft5 VALUES('hello', 'world');
+ }
+ do_recover_test 9
+}
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 10.1 {
+ CREATE TABLE x1(a PRIMARY KEY, str TEXT) WITHOUT ROWID;
+ INSERT INTO x1 VALUES(1, '
+ \nhello\012world(\n0)(\n1)
+ ');
+ INSERT INTO x1 VALUES(2, '
+ \nhello
+ ');
+}
+do_execsql_test 10.2 "
+ INSERT INTO x1 VALUES(3, '\012hello there\015world');
+ INSERT INTO x1 VALUES(4, '\015hello there\015world');
+"
+do_recover_test 10
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 11.1 {
+ PRAGMA page_size = 4096;
+ PRAGMA encoding='utf16';
+ PRAGMA auto_vacuum = 2;
+ PRAGMA user_version = 45;
+ PRAGMA application_id = 22;
+
+ CREATE TABLE u1(u, v);
+ INSERT INTO u1 VALUES('edvin marton', 'bond');
+ INSERT INTO u1 VALUES(1, 4.0);
+}
+do_execsql_test 11.1a {
+ PRAGMA auto_vacuum;
+} {2}
+
+do_recover_test 11
+
+do_test 12.1 {
+ set R [sqlite3_recover_init db "" test.db2]
+ $R config lostandfound ""
+ $R config invalid xyz
+} {12}
+do_test 12.2 {
+ $R run
+ $R run
+} {0}
+
+do_test 12.3 {
+ $R finish
+} {}
+
+
+
+#-------------------------------------------------------------------------
+reset_db
+file_control_reservebytes db 16
+do_execsql_test 12.1 {
+ PRAGMA auto_vacuum = 2;
+ PRAGMA user_version = 45;
+ PRAGMA application_id = 22;
+
+ CREATE TABLE u1(u, v);
+ CREATE UNIQUE INDEX i1 ON u1(u, v);
+ INSERT INTO u1 VALUES(1, 2), (3, 4);
+
+ CREATE TABLE u2(u, v);
+ CREATE UNIQUE INDEX i2 ON u1(u, v);
+ INSERT INTO u2 VALUES(hex(randomblob(500)), hex(randomblob(1000)));
+ INSERT INTO u2 VALUES(hex(randomblob(500)), hex(randomblob(1000)));
+ INSERT INTO u2 VALUES(hex(randomblob(500)), hex(randomblob(1000)));
+ INSERT INTO u2 VALUES(hex(randomblob(50000)), hex(randomblob(20000)));
+}
+
+do_recover_test 12
+
+#-------------------------------------------------------------------------
+reset_db
+sqlite3 db ""
+do_recover_test 13
+
+do_execsql_test 14.1 {
+ PRAGMA auto_vacuum = 2;
+ PRAGMA user_version = 45;
+ PRAGMA application_id = 22;
+
+ CREATE TABLE u1(u, v);
+ CREATE UNIQUE INDEX i1 ON u1(u, v);
+ INSERT INTO u1 VALUES(1, 2), (3, 4);
+
+ CREATE TABLE u2(u, v);
+ CREATE UNIQUE INDEX i2 ON u1(u, v);
+ INSERT INTO u2 VALUES(hex(randomblob(500)), hex(randomblob(1000)));
+ INSERT INTO u2 VALUES(hex(randomblob(500)), hex(randomblob(1000)));
+ INSERT INTO u2 VALUES(hex(randomblob(500)), hex(randomblob(1000)));
+ INSERT INTO u2 VALUES(hex(randomblob(50000)), hex(randomblob(20000)));
+}
+do_recover_test 14
+
+#-------------------------------------------------------------------------
+reset_db
+execsql {
+ PRAGMA journal_mode=OFF;
+ PRAGMA mmap_size=10;
+}
+do_execsql_test 15.1 {
+ CREATE TABLE t1(x);
+} {}
+do_recover_test 15
+
+#-------------------------------------------------------------------------
+reset_db
+if {[wal_is_capable]} {
+ do_execsql_test 16.1 {
+ PRAGMA journal_mode = wal;
+ CREATE TABLE t1(x);
+ INSERT INTO t1 VALUES(1), (2), (3);
+ } {wal}
+ do_test 16.2 {
+ set R [sqlite3_recover_init db main test.db2]
+ $R run
+ $R finish
+ } {}
+ do_execsql_test 16.3 {
+ SELECT * FROM t1;
+ } {1 2 3}
+
+ do_execsql_test 16.4 {
+ BEGIN;
+ SELECT * FROM t1;
+ } {1 2 3}
+ do_test 16.5 {
+ set R [sqlite3_recover_init db main test.db2]
+ $R run
+ list [catch { $R finish } msg] $msg
+ } {1 {cannot start a transaction within a transaction}}
+ do_execsql_test 16.6 {
+ SELECT * FROM t1;
+ } {1 2 3}
+ do_execsql_test 16.7 {
+ INSERT INTO t1 VALUES(4);
+ }
+ do_test 16.8 {
+ set R [sqlite3_recover_init db main test.db2]
+ $R run
+ list [catch { $R finish } msg] $msg
+ } {1 {cannot start a transaction within a transaction}}
+ do_execsql_test 16.9 {
+ SELECT * FROM t1;
+ COMMIT;
+ } {1 2 3 4}
+}
+
+finish_test
+
diff --git a/ext/recover/recover_common.tcl b/ext/recover/recover_common.tcl
new file mode 100644
index 0000000..fdf735e
--- /dev/null
+++ b/ext/recover/recover_common.tcl
@@ -0,0 +1,14 @@
+
+
+if {![info exists testdir]} {
+ set testdir [file join [file dirname [info script]] .. .. test]
+}
+source $testdir/tester.tcl
+
+if {[info commands sqlite3_recover_init]==""} {
+ finish_test
+ return -code return
+}
+
+
+
diff --git a/ext/recover/recoverclobber.test b/ext/recover/recoverclobber.test
new file mode 100644
index 0000000..e096b2e
--- /dev/null
+++ b/ext/recover/recoverclobber.test
@@ -0,0 +1,50 @@
+# 2019 April 23
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+# Tests for the SQLITE_RECOVER_ROWIDS option.
+#
+
+source [file join [file dirname [info script]] recover_common.tcl]
+set testprefix recoverclobber
+
+proc recover {db output} {
+ set R [sqlite3_recover_init db main test.db2]
+ $R run
+ $R finish
+}
+
+forcedelete test.db2
+do_execsql_test 1.0 {
+ ATTACH 'test.db2' AS aux;
+ CREATE TABLE aux.x1(x, one);
+ INSERT INTO x1 VALUES(1, 'one'), (2, 'two'), (3, 'three');
+
+ CREATE TABLE t1(a, b);
+ INSERT INTO t1 VALUES(1, 1), (2, 2), (3, 3), (4, 4);
+
+ DETACH aux;
+}
+
+breakpoint
+do_test 1.1 {
+ recover db test.db2
+} {}
+
+do_execsql_test 1.2 {
+ ATTACH 'test.db2' AS aux;
+ SELECT * FROM aux.t1;
+} {1 1 2 2 3 3 4 4}
+
+do_catchsql_test 1.3 {
+ SELECT * FROM aux.x1;
+} {1 {no such table: aux.x1}}
+
+finish_test
diff --git a/ext/recover/recovercorrupt.test b/ext/recover/recovercorrupt.test
new file mode 100644
index 0000000..eb6fe53
--- /dev/null
+++ b/ext/recover/recovercorrupt.test
@@ -0,0 +1,67 @@
+# 2022 August 28
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+
+source [file join [file dirname [info script]] recover_common.tcl]
+set testprefix recovercorrupt
+
+database_may_be_corrupt
+
+do_execsql_test 1.0 {
+ PRAGMA page_size = 512;
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c);
+ INSERT INTO t1 VALUES(1, 2, 3);
+ INSERT INTO t1 VALUES(2, hex(randomblob(100)), randomblob(200));
+ CREATE INDEX i1 ON t1(b, c);
+ CREATE TABLE t2(a PRIMARY KEY, b, c) WITHOUT ROWID;
+ INSERT INTO t2 VALUES(1, 2, 3);
+ INSERT INTO t2 VALUES(2, hex(randomblob(100)), randomblob(200));
+ ANALYZE;
+ PRAGMA writable_schema = 1;
+ DELETE FROM sqlite_schema WHERE name='t2';
+}
+
+do_test 1.1 {
+ expr [file size test.db]>3072
+} {1}
+
+proc toggle_bit {blob bit} {
+ set byte [expr {$bit / 8}]
+ set bit [expr {$bit & 0x0F}]
+ binary scan $blob a${byte}ca* A x B
+ set x [expr {$x ^ (1 << $bit)}]
+ binary format a*ca* $A $x $B
+}
+
+
+db_save_and_close
+for {set ii 0} {$ii < 10000} {incr ii} {
+ db_restore_and_reopen
+ db func toggle_bit toggle_bit
+ set bitsperpage [expr 512*8]
+
+ set pg [expr {($ii / $bitsperpage)+1}]
+ set byte [expr {$ii % $bitsperpage}]
+ db eval {
+ UPDATE sqlite_dbpage SET data = toggle_bit(data, $byte) WHERE pgno=$pg
+ }
+
+ set R [sqlite3_recover_init db main test.db2]
+ $R config lostandfound lost_and_found
+ $R run
+ do_test 1.2.$ii {
+ $R finish
+ } {}
+}
+
+
+finish_test
+
diff --git a/ext/recover/recovercorrupt2.test b/ext/recover/recovercorrupt2.test
new file mode 100644
index 0000000..7147c67
--- /dev/null
+++ b/ext/recover/recovercorrupt2.test
@@ -0,0 +1,289 @@
+# 2022 August 28
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+
+source [file join [file dirname [info script]] recover_common.tcl]
+set testprefix recovercorrupt2
+
+do_execsql_test 1.0 {
+ PRAGMA page_size = 512;
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c);
+ INSERT INTO t1 VALUES(1, 2, 3);
+ INSERT INTO t1 VALUES(2, hex(randomblob(100)), randomblob(200));
+ CREATE INDEX i1 ON t1(b, c);
+ CREATE TABLE t2(a PRIMARY KEY, b, c) WITHOUT ROWID;
+ INSERT INTO t2 VALUES(1, 2, 3);
+ INSERT INTO t2 VALUES(2, hex(randomblob(100)), randomblob(200));
+ ANALYZE;
+ PRAGMA writable_schema = 1;
+ UPDATE sqlite_schema SET sql = 'CREATE INDEX i1 ON o(world)' WHERE name='i1';
+ DELETE FROM sqlite_schema WHERE name='sqlite_stat4';
+}
+
+do_test 1.1 {
+ set R [sqlite3_recover_init db main test.db2]
+ $R run
+ $R finish
+} {}
+
+sqlite3 db2 test.db2
+do_execsql_test -db db2 1.2 {
+ SELECT sql FROM sqlite_schema
+} {
+ {CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c)}
+ {CREATE TABLE t2(a PRIMARY KEY, b, c) WITHOUT ROWID}
+ {CREATE TABLE sqlite_stat1(tbl,idx,stat)}
+}
+db2 close
+
+do_execsql_test 1.3 {
+ PRAGMA writable_schema = 1;
+ UPDATE sqlite_schema SET sql = 'CREATE TABLE t2 syntax error!' WHERE name='t2';
+}
+
+do_test 1.4 {
+ set R [sqlite3_recover_init db main test.db2]
+ $R run
+ $R finish
+} {}
+
+sqlite3 db2 test.db2
+do_execsql_test -db db2 1.5 {
+ SELECT sql FROM sqlite_schema
+} {
+ {CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c)}
+ {CREATE TABLE sqlite_stat1(tbl,idx,stat)}
+}
+db2 close
+
+#-------------------------------------------------------------------------
+#
+reset_db
+do_test 2.0 {
+ sqlite3 db {}
+ db deserialize [decode_hexdb {
+| size 8192 pagesize 4096 filename x3.db
+| page 1 offset 0
+| 0: 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00 SQLite format 3.
+| 16: 10 00 01 01 00 40 20 20 00 00 00 02 00 00 00 02 .....@ ........
+| 32: 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 04 ................
+| 48: 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 ................
+| 80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 ................
+| 96: 00 2e 63 00 0d 00 00 00 01 0f d8 00 0f d8 00 00 ..c.............
+| 4048: 00 00 00 00 00 00 00 00 26 01 06 17 11 11 01 39 ........&......9
+| 4064: 74 61 62 6c 65 74 31 74 31 02 43 52 45 41 54 45 tablet1t1.CREATE
+| 4080: 20 54 41 42 4c 45 20 74 31 28 61 2c 62 2c 63 29 TABLE t1(a,b,c)
+| page 2 offset 4096
+| 0: 0d 00 00 00 01 0f ce 00 0f ce 00 00 00 00 00 00 ................
+| 4032: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff ff ..............(.
+| 4048: ff ff ff ff ff ff ff 28 04 27 25 23 61 61 61 61 .........'%#aaaa
+| 4064: 61 61 61 61 61 61 61 61 61 62 62 62 62 62 62 62 aaaaaaaaabbbbbbb
+| 4080: 62 62 62 62 62 63 63 63 63 63 63 63 63 63 63 63 bbbbbccccccccccc
+| end x3.db
+}]} {}
+
+do_test 2.1 {
+ set R [sqlite3_recover_init db main test.db2]
+ $R run
+ $R finish
+} {}
+
+sqlite3 db2 test.db2
+do_execsql_test -db db2 2.2 {
+ SELECT sql FROM sqlite_schema
+} {
+ {CREATE TABLE t1(a,b,c)}
+}
+do_execsql_test -db db2 2.3 {
+ SELECT * FROM t1
+} {}
+db2 close
+
+#-------------------------------------------------------------------------
+#
+reset_db
+do_test 3.0 {
+ sqlite3 db {}
+ db deserialize [decode_hexdb {
+ .open --hexdb
+ | size 4096 pagesize 1024 filename corrupt032.txt.db
+ | page 1 offset 0
+ | 0: 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00 SQLite format 3.
+ | 16: 04 00 01 01 08 40 20 20 00 00 00 02 00 00 00 03 .....@ ........
+ | 32: 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 04 ................
+ | 48: 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 00 ................
+ | 80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 ................
+ | 96: 00 2e 24 80 0d 00 00 00 01 03 d4 00 03 d4 00 00 ..$.............
+ | 976: 00 00 00 00 22 01 06 17 11 11 01 31 74 61 62 6c ...........1tabl
+ | 992: 65 74 31 74 31 02 43 52 45 41 54 45 20 54 41 42 et1t1.CREATE TAB
+ | 1008: 4c 45 20 74 31 28 78 29 00 00 00 00 00 00 00 00 LE t1(x)........
+ | page 2 offset 1024
+ | 0: 0d 00 00 00 01 02 06 00 02 06 00 00 00 00 00 00 ................
+ | 512: 00 00 00 00 00 00 8b 60 01 03 97 46 00 00 00 00 .......`...F....
+ | 1008: 00 00 00 00 00 00 00 03 00 00 00 00 00 00 00 00 ................
+ | end corrupt032.txt.db
+}]} {}
+
+do_test 3.1 {
+ set R [sqlite3_recover_init db main test.db2]
+ $R run
+ $R finish
+} {}
+
+#-------------------------------------------------------------------------
+#
+reset_db
+do_test 4.0 {
+ sqlite3 db {}
+ db deserialize [decode_hexdb {
+ .open --hexdb
+ | size 4096 pagesize 4096 filename crash-00f2d3627f1b43.db
+ | page 1 offset 0
+ | 0: 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00 SQLite format 3.
+ | 16: 00 01 01 02 00 40 20 20 01 00 ff 00 42 01 10 01 .....@ ....B...
+ | 32: ef 00 00 87 00 ff ff ff f0 01 01 10 ff ff 00 00 ................
+ | end crash-00f2d3627f1b43.db
+}]} {}
+
+do_test 4.1 {
+ set R [sqlite3_recover_init db main test.db2]
+ catch { $R run }
+ list [catch { $R finish } msg] $msg
+} {1 {unable to open database file}}
+
+#-------------------------------------------------------------------------
+#
+reset_db
+do_test 5.0 {
+ sqlite3 db {}
+ db deserialize [decode_hexdb {
+.open --hexdb
+| size 16384 pagesize 4096 filename crash-7b75760a4c5f15.db
+| page 1 offset 0
+| 0: 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00 SQLite format 3.
+| 16: 10 00 01 01 00 40 20 20 00 00 00 00 00 00 00 04 .....@ ........
+| 32: 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00 00 ................
+| 96: 00 00 00 00 0d 00 00 00 03 0f 4e 00 0f bc 0f 90 ..........N.....
+| 112: 0f 4e 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .N..............
+| 3904: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 40 03 ..............@.
+| 3920: 06 17 11 11 01 6d 74 61 62 6c 65 74 32 74 32 04 .....mtablet2t2.
+| 3936: 43 52 45 41 54 45 20 54 41 42 4c 45 20 74 32 28 CREATE TABLE t2(
+| 3952: 78 2c 79 2c 7a 20 50 52 49 4d 41 52 59 20 4b 45 x,y,z PRIMARY KE
+| 3968: 59 29 20 57 49 54 48 4f 55 54 20 52 4f 57 49 44 Y) WITHOUT ROWID
+| 3984: 2a 02 06 17 13 11 01 3f 69 6e 64 65 78 74 31 61 *......?indext1a
+| 4000: 74 31 03 43 52 45 41 54 45 20 49 4e 44 45 58 20 t1.CREATE INDEX
+| 4016: 74 31 61 20 4f 4e 20 74 31 28 61 29 42 01 06 17 t1a ON t1(a)B...
+| 4032: 11 11 01 71 74 61 62 6c 65 74 31 74 31 02 43 52 ...qtablet1t1.CR
+| 4048: 45 41 54 45 20 54 41 42 4c 45 20 74 31 28 61 20 EATE TABLE t1(a
+| 4064: 49 4e 54 2c 62 20 54 45 58 54 2c 63 20 42 4c 4f INT,b TEXT,c BLO
+| 4080: 42 2c 64 20 52 45 41 4c 29 20 53 54 52 49 43 54 B,d REAL) STRICT
+| page 2 offset 4096
+| 0: 0d 00 00 00 14 0c ae 00 0f df 0f bd 0f 9a 0f 76 ...............v
+| 16: 0f 51 0f 2b 0f 04 0e dc 0e b3 0e 89 0e 5e 0e 32 .Q.+.........^.2
+| 32: 0e 05 0d 1a 0d a8 0d 78 0d 47 0d 15 0c e2 00 00 .......x.G......
+| 3232: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 32 14 ..............2.
+| 3248: 05 06 3f 34 07 15 f4 c9 23 af e2 b3 b6 61 62 63 ..?4....#....abc
+| 3264: 30 32 30 78 79 7a 01 00 00 00 00 00 00 00 00 00 020xyz..........
+| 3280: 00 00 00 00 00 00 00 00 00 00 c3 b0 96 7e fb 4e .............~.N
+| 3296: c5 4c 31 13 05 06 1f 32 07 dd f2 2a a5 7e b2 4d .L1....2...*.~.M
+| 3312: 82 61 62 63 30 31 39 78 79 7a 01 00 00 00 00 00 .abc019xyz......
+| 3328: 00 00 00 00 00 00 00 00 00 00 00 00 00 c3 a3 d6 ................
+| 3344: e9 f1 c2 fd f3 30 12 05 06 1f 30 07 8f 8f f5 c4 .....0....0.....
+| 3360: 35 b6 7f 8d 61 62 63 30 31 38 00 00 00 00 00 00 5...abc018......
+| 3376: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 43 ...............C
+| 3392: b2 13 1f 9d 56 8a 47 21 b1 05 06 1f 2e 07 7f 46 ....V.G!.......F
+| 3408: 91 03 3f 97 fb f7 61 62 63 30 00 00 00 00 00 00 ..?...abc0......
+| 3440: c3 bb d8 96 86 c2 e8 2b 2e 10 05 06 1f 2c 07 6d .......+.....,.m
+| 3456: 85 7b ce d0 32 d2 54 61 62 63 30 00 00 00 00 00 ....2.Tabc0.....
+| 3488: 43 a1 eb 44 14 dc 03 7b 2d 0f 05 06 1f 2a 07 d9 C..D....-....*..
+| 3504: ab ec bf 34 51 70 f3 61 62 63 30 31 35 78 79 7a ...4Qp.abc015xyz
+| 3520: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 c3 ................
+| 3536: b6 3d f4 46 b1 6a af 2c 0e 05 06 1f 28 07 36 75 .=.F.j.,....(.6u
+| 3552: e9 a2 bd 05 04 ea 61 62 63 30 31 34 78 79 7a 00 ......abc014xyz.
+| 3568: 00 00 00 00 00 00 00 00 00 00 00 00 00 c3 ab 23 ...............#
+| 3584: a7 6a 34 ca f8 2b 0d 05 06 1f 26 07 48 45 ab e0 .j4..+....&.HE..
+| 3600: 8c 7c ff 0c 61 62 63 30 31 33 78 79 7a 00 00 00 .|..abc013xyz...
+| 3616: 00 00 00 00 0d d0 00 00 00 00 43 b8 d3 93 f4 92 ..........C.....
+| 3632: 5b 7a 2a 0c 05 06 1f 24 07 be 6d 1e db 61 5d 80 [z*....$..m..a].
+| 3648: 9f 61 62 63 30 31 32 78 79 7a 00 00 00 00 00 00 .abc012xyz......
+| 3664: 00 00 00 00 00 00 43 b5 a1 a4 af 7b c6 60 29 0b ......C......`).
+| 3680: 05 06 1f 22 07 6e a2 a3 64 68 d4 a6 bd 61 62 63 .....n..dh...abc
+| 3696: 30 31 31 78 79 7a 00 00 00 00 00 00 00 00 00 00 011xyz..........
+| 3712: 00 c3 c4 1e ff 0f fc e6 ff 28 0a 05 06 1f 20 07 .........(.... .
+| 3728: 50 f9 4a bb a5 7a 1e ca 61 62 63 30 31 30 78 79 P.J..z..abc010xy
+| 3744: 7a 00 00 00 00 00 00 00 00 00 00 c3 a7 90 ed d9 z...............
+| 3760: 5c 2c d5 27 09 05 06 1f 1e 07 90 8e 1d d9 1c 3a .,.'...........:
+| 3776: e8 c1 61 62 63 30 30 39 78 79 7a 00 00 00 00 00 ..abc009xyz.....
+| 3792: 00 00 00 00 43 a7 97 87 cf b0 ff 79 26 08 05 06 ....C......y&...
+| 3808: 1f 1c 07 86 65 f6 7c 50 7a 2c 76 61 62 63 30 30 ....e.|Pz,vabc00
+| 3824: 38 78 79 7a 00 00 00 00 00 00 00 00 c3 b0 e3 4c 8xyz...........L
+| 3840: 4f d3 41 b5 25 07 05 06 1f 1a 07 8b 20 e5 68 11 O.A.%....... .h.
+| 3856: 13 55 87 61 62 63 30 30 37 78 79 7a 00 00 00 00 .U.abc007xyz....
+| 3872: 00 00 00 c3 b6 a3 74 f1 9c 33 f8 24 06 05 06 1f ......t..3.$....
+| 3888: 18 07 97 3c bc 34 49 94 54 ab 61 62 63 30 30 36 ...<.4I.T.abc006
+| 3904: 78 79 7a 00 00 00 00 00 00 c3 88 00 c2 ca 4c 4d xyz...........LM
+| 3920: d3 23 05 05 06 1f 16 07 59 37 11 10 e9 e5 3d d5 .#......Y7....=.
+| 3936: 61 62 63 30 30 35 78 79 7a 00 00 00 00 00 c3 c0 abc005xyz.......
+| 3952: 15 12 67 ed 4b 79 22 04 05 06 1f 14 07 93 39 01 ..g.Ky........9.
+| 3968: 7f b8 c7 99 58 61 62 63 30 30 34 78 79 7a 00 00 ....Xabc004xyz..
+| 3984: 09 c0 43 bf e0 e7 6d 70 fd 61 21 03 05 06 1f 12 ..C...mp.a!.....
+| 4000: 07 b6 df 8d 8b 27 08 22 5a 61 62 63 30 30 33 78 .....'..Zabc003x
+| 4016: 79 7a 00 00 00 c3 c7 ea 0f dc dd 32 22 20 02 05 yz.........2. ..
+| 4032: 06 1f 10 07 2f a6 da 71 df 66 b3 b5 61 62 63 30 ..../..q.f..abc0
+| 4048: 30 32 78 79 7a 00 00 c3 ce d9 8d e9 ec 20 45 1f 02xyz........ E.
+| 4064: 01 05 06 1f 0e 07 5a 47 53 20 3b 48 8f c0 61 62 ......ZGS ;H..ab
+| 4080: 63 30 30 31 78 79 7a 00 c3 c9 e6 81 f8 d9 24 04 c001xyz.......$.
+| page 3 offset 8192
+| 0: 0a 00 00 00 14 0e fd 00 0f f3 0f e6 0f d9 0f cc ................
+| 16: 0f bf 0f b2 0f a5 0f 98 0f 8b 0f 7e 0f 71 0f 64 ...........~.q.d
+| 32: 0f 57 0f 4a 0f 3d 0f 30 0f 24 00 00 00 00 00 00 .W.J.=.0.$......
+| 3824: 00 00 00 00 00 00 00 00 00 00 00 00 00 0c 03 06 ................
+| 3840: 01 7f 46 91 03 3f 97 fb f7 11 0c 03 06 01 6e a2 ..F..?........n.
+| 3856: a3 64 68 d4 a6 bd 0b 0c 03 06 01 6d 85 7b ce d0 .dh........m....
+| 3872: 32 d2 54 10 0b 03 06 09 5a 47 53 20 3b 48 8f c0 2.T.....ZGS ;H..
+| 3888: 0c 03 06 01 59 37 11 10 e9 e5 3d d5 05 0c 03 06 ....Y7....=.....
+| 3904: 01 50 f9 4a bb a5 7a 1e ca 0a 0c 03 06 01 48 45 .P.J..z.......HE
+| 3920: ab e0 8c 7c ff 0c 0d 0c 03 06 01 36 75 e9 a2 bd ...|.......6u...
+| 3936: 05 04 ea 0e 0c 03 06 01 2f a6 da 71 df 66 b3 b5 ......../..q.f..
+| 3952: 02 0c 03 06 01 15 f4 c9 23 af e2 b3 b6 14 0c 03 ........#.......
+| 3968: 06 01 dd f2 2a a5 7e b2 4d 82 13 0c 03 06 01 d9 ....*.~.M.......
+| 3984: ab ec bf 34 51 70 f3 0f 0c 03 06 01 be 6d 1e db ...4Qp.......m..
+| 4000: 61 5d 80 9f 0c 0c 03 06 01 b6 df 8d 8b 27 08 22 a]...........'..
+| 4016: 5a 03 0c 03 06 01 97 3c bc 34 49 94 54 ab 06 0c Z......<.4I.T...
+| 4032: 03 06 01 93 39 01 7f b8 c7 99 58 04 0c 03 06 01 ....9.....X.....
+| 4048: 90 8e 1d d9 1c 3a e8 c1 09 0c 03 06 01 8f 8f f5 .....:..........
+| 4064: c4 35 b6 7f 8d 12 0c 03 06 01 8b 20 e5 68 11 13 .5......... .h..
+| 4080: 55 87 07 0c 03 06 01 86 65 f6 7c 50 7a 2b 06 08 U.......e.|Pz+..
+| page 4 offset 12288
+| 0: 0a 00 00 00 14 0f 62 00 0f 7a 0f a1 0f c9 0f d9 ......b..z......
+| 16: 0f 81 0f d1 0f f1 0f f9 0f e1 0f 89 0e 6a 0f c1 .............j..
+| 32: 0f 91 0f 99 0f b9 0f 72 0f 62 0f e9 0f b1 0f a9 .......r.b......
+| 3936: 00 00 07 04 01 01 01 11 0e 9e 07 04 01 01 01 0b ................
+| 3952: 31 16 07 04 01 01 01 10 37 36 06 04 09 01 01 ab 1.......76......
+| 3968: 58 07 04 01 01 01 05 1c 28 07 04 01 01 01 0a 10 X.......(.......
+| 3984: cf 07 04 01 01 01 0d b2 e3 07 04 01 01 01 0e d3 ................
+| 4000: f2 07 04 01 01 01 02 41 ad 07 04 01 01 01 14 3e .......A.......>
+| 4016: 22 07 04 01 01 01 13 27 45 07 04 01 01 01 0f ad .......'E.......
+| 4032: dd 07 04 01 01 01 0c 2e a1 07 04 01 01 01 03 df ................
+| 4048: e1 07 04 01 01 01 06 59 a7 07 04 01 01 01 04 27 .......Y.......'
+| 4064: bd 07 04 01 01 01 09 d0 e0 07 04 01 01 01 12 39 ...............9
+| 4080: 4f 07 04 01 01 01 07 c4 11 06 04 00 00 00 00 00 O...............
+| end crash-7b75760a4c5f15.db
+}]} {}
+
+do_test 5.1 {
+ set R [sqlite3_recover_init db main test.db2]
+ catch { $R run }
+ list [catch { $R finish } msg] $msg
+} {0 {}}
+
+finish_test
+
diff --git a/ext/recover/recoverfault.test b/ext/recover/recoverfault.test
new file mode 100644
index 0000000..30bb655
--- /dev/null
+++ b/ext/recover/recoverfault.test
@@ -0,0 +1,84 @@
+# 2022 August 28
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+
+source [file join [file dirname [info script]] recover_common.tcl]
+set testprefix recoverfault
+
+
+#--------------------------------------------------------------------------
+proc compare_result {db1 db2 sql} {
+ set r1 [$db1 eval $sql]
+ set r2 [$db2 eval $sql]
+ if {$r1 != $r2} {
+ puts "r1: $r1"
+ puts "r2: $r2"
+ error "mismatch for $sql"
+ }
+ return ""
+}
+
+proc compare_dbs {db1 db2} {
+ compare_result $db1 $db2 "SELECT sql FROM sqlite_master ORDER BY 1"
+ foreach tbl [$db1 eval {SELECT name FROM sqlite_master WHERE type='table'}] {
+ compare_result $db1 $db2 "SELECT * FROM $tbl"
+ }
+}
+#--------------------------------------------------------------------------
+
+do_execsql_test 1.0 {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c);
+ INSERT INTO t1 VALUES(1, 2, 3);
+ INSERT INTO t1 VALUES(2, hex(randomblob(1000)), randomblob(2000));
+ CREATE INDEX i1 ON t1(b, c);
+ ANALYZE;
+}
+faultsim_save_and_close
+
+do_faultsim_test 1 -faults oom* -prep {
+ catch { db2 close }
+ faultsim_restore_and_reopen
+} -body {
+ set R [sqlite3_recover_init db main test.db2]
+ $R run
+ $R finish
+} -test {
+ faultsim_test_result {0 {}} {1 {}}
+ if {$testrc==0} {
+ sqlite3 db2 test.db2
+ compare_dbs db db2
+ db2 close
+ }
+}
+
+faultsim_restore_and_reopen
+do_execsql_test 2.0 {
+ CREATE TABLE t2(a INTEGER PRIMARY KEY, b, c);
+ INSERT INTO t2 VALUES(1, 2, 3);
+ INSERT INTO t2 VALUES(2, hex(randomblob(1000)), hex(randomblob(2000)));
+ PRAGMA writable_schema = 1;
+ DELETE FROM sqlite_schema WHERE name='t2';
+}
+faultsim_save_and_close
+
+do_faultsim_test 2 -faults oom* -prep {
+ faultsim_restore_and_reopen
+} -body {
+ set R [sqlite3_recover_init db main test.db2]
+ $R config lostandfound lost_and_found
+ $R run
+ $R finish
+} -test {
+ faultsim_test_result {0 {}} {1 {}}
+}
+
+finish_test
+
diff --git a/ext/recover/recoverfault2.test b/ext/recover/recoverfault2.test
new file mode 100644
index 0000000..e80d480
--- /dev/null
+++ b/ext/recover/recoverfault2.test
@@ -0,0 +1,102 @@
+# 2022 August 28
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+
+source [file join [file dirname [info script]] recover_common.tcl]
+set testprefix recoverfault2
+
+
+#--------------------------------------------------------------------------
+proc compare_result {db1 db2 sql} {
+ set r1 [$db1 eval $sql]
+ set r2 [$db2 eval $sql]
+ if {$r1 != $r2} {
+ puts "r1: $r1"
+ puts "r2: $r2"
+ error "mismatch for $sql"
+ }
+ return ""
+}
+
+proc compare_dbs {db1 db2} {
+ compare_result $db1 $db2 "SELECT sql FROM sqlite_master ORDER BY 1"
+ foreach tbl [$db1 eval {SELECT name FROM sqlite_master WHERE type='table'}] {
+ compare_result $db1 $db2 "SELECT * FROM $tbl"
+ }
+}
+#--------------------------------------------------------------------------
+
+do_execsql_test 1.0 "
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b);
+ INSERT INTO t1 VALUES(2, '\012hello\015world\012today\n');
+"
+faultsim_save_and_close
+
+proc my_sql_hook {sql} {
+ lappend ::lSql $sql
+ return 0
+}
+
+do_faultsim_test 1 -faults oom* -prep {
+ catch { db2 close }
+ faultsim_restore_and_reopen
+ set ::lSql [list]
+} -body {
+ set R [sqlite3_recover_init_sql db main my_sql_hook]
+ $R run
+ $R finish
+} -test {
+ faultsim_test_result {0 {}} {1 {}}
+ if {$testrc==0} {
+ sqlite3 db2 ""
+ db2 eval [join $::lSql ";"]
+ compare_dbs db db2
+ db2 close
+ }
+}
+
+ifcapable utf16 {
+ reset_db
+ do_execsql_test 2.0 "
+ PRAGMA encoding='utf-16';
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b);
+ INSERT INTO t1 VALUES(2, '\012hello\015world\012today\n');
+ "
+ faultsim_save_and_close
+
+ proc my_sql_hook {sql} {
+ lappend ::lSql $sql
+ return 0
+ }
+
+ do_faultsim_test 2 -faults oom-t* -prep {
+ catch { db2 close }
+ faultsim_restore_and_reopen
+ set ::lSql [list]
+ } -body {
+ set R [sqlite3_recover_init_sql db main my_sql_hook]
+ $R run
+ $R finish
+ } -test {
+ faultsim_test_result {0 {}} {1 {}}
+ if {$testrc==0} {
+ sqlite3 db2 ""
+ db2 eval [join $::lSql ";"]
+ compare_dbs db db2
+ db2 close
+ }
+ }
+}
+
+
+
+finish_test
+
diff --git a/ext/recover/recoverold.test b/ext/recover/recoverold.test
new file mode 100644
index 0000000..c6acbb2
--- /dev/null
+++ b/ext/recover/recoverold.test
@@ -0,0 +1,189 @@
+# 2019 April 23
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+#
+
+source [file join [file dirname [info script]] recover_common.tcl]
+set testprefix recoverold
+
+proc compare_result {db1 db2 sql} {
+ set r1 [$db1 eval $sql]
+ set r2 [$db2 eval $sql]
+ if {$r1 != $r2} {
+ puts "sql: $sql"
+ puts "r1: $r1"
+ puts "r2: $r2"
+ error "mismatch for $sql"
+ }
+ return ""
+}
+
+proc compare_dbs {db1 db2} {
+ compare_result $db1 $db2 "SELECT sql FROM sqlite_master ORDER BY 1"
+ foreach tbl [$db1 eval {SELECT name FROM sqlite_master WHERE type='table'}] {
+ compare_result $db1 $db2 "SELECT * FROM $tbl"
+ }
+}
+
+proc do_recover_test {tn {tsql {}} {res {}}} {
+ forcedelete test.db2
+ forcedelete rstate.db
+
+ set R [sqlite3_recover_init db main test.db2]
+ $R config lostandfound lost_and_found
+ $R run
+ $R finish
+
+ sqlite3 db2 test.db2
+
+ if {$tsql==""} {
+ uplevel [list do_test $tn.1 [list compare_dbs db db2] {}]
+ } else {
+ uplevel [list do_execsql_test -db db2 $tn.1 $tsql $res]
+ }
+ db2 close
+
+ forcedelete test.db2
+ forcedelete rstate.db
+
+ set ::sqlhook [list]
+ set R [sqlite3_recover_init_sql db main my_sql_hook]
+ $R config lostandfound lost_and_found
+ $R run
+ $R finish
+
+ sqlite3 db2 test.db2
+ db2 eval [join $::sqlhook ";"]
+
+
+ db cache flush
+ if {$tsql==""} {
+ compare_dbs db db2
+ uplevel [list do_test $tn.sql [list compare_dbs db db2] {}]
+ } else {
+ uplevel [list do_execsql_test -db db2 $tn.sql $tsql $res]
+ }
+ db2 close
+}
+
+proc my_sql_hook {sql} {
+ lappend ::sqlhook $sql
+ return 0
+}
+
+
+set doc {
+ hello
+ world
+}
+do_execsql_test 1.1.1 {
+ CREATE TABLE t1(a INTEGER PRIMARY KEY, b, c);
+ INSERT INTO t1 VALUES(1, 4, X'1234567800');
+ INSERT INTO t1 VALUES(2, 'test', 8.1);
+ INSERT INTO t1 VALUES(3, $doc, 8.4);
+}
+do_recover_test 1.1.2
+
+do_execsql_test 1.2.1 "
+ DELETE FROM t1;
+ INSERT INTO t1 VALUES(13, 'hello\r\nworld', 13);
+"
+do_recover_test 1.2.2
+
+do_execsql_test 1.3.1 "
+ CREATE TABLE t2(i INTEGER PRIMARY KEY AUTOINCREMENT, b, c);
+ INSERT INTO t2 VALUES(NULL, 1, 2);
+ INSERT INTO t2 VALUES(NULL, 3, 4);
+ INSERT INTO t2 VALUES(NULL, 5, 6);
+ CREATE TABLE t3(i INTEGER PRIMARY KEY AUTOINCREMENT, b, c);
+ INSERT INTO t3 VALUES(NULL, 1, 2);
+ INSERT INTO t3 VALUES(NULL, 3, 4);
+ INSERT INTO t3 VALUES(NULL, 5, 6);
+ DELETE FROM t2;
+"
+do_recover_test 1.3.2
+
+#-------------------------------------------------------------------------
+reset_db
+do_execsql_test 2.1.0 {
+ PRAGMA auto_vacuum = 0;
+ CREATE TABLE t1(a, b, c, PRIMARY KEY(b, c)) WITHOUT ROWID;
+ INSERT INTO t1 VALUES(1, 2, 3);
+ INSERT INTO t1 VALUES(4, 5, 6);
+ INSERT INTO t1 VALUES(7, 8, 9);
+}
+
+do_recover_test 2.1.1
+
+do_execsql_test 2.2.0 {
+ PRAGMA writable_schema = 1;
+ DELETE FROM sqlite_master WHERE name='t1';
+}
+do_recover_test 2.2.1 {
+ SELECT name FROM sqlite_master
+} {lost_and_found}
+
+do_execsql_test 2.3.0 {
+ CREATE TABLE lost_and_found(a, b, c);
+}
+do_recover_test 2.3.1 {
+ SELECT name FROM sqlite_master
+} {lost_and_found lost_and_found_0}
+
+do_execsql_test 2.4.0 {
+ CREATE TABLE lost_and_found_0(a, b, c);
+}
+do_recover_test 2.4.1 {
+ SELECT name FROM sqlite_master;
+ SELECT * FROM lost_and_found_1;
+} {lost_and_found lost_and_found_0 lost_and_found_1
+ 2 2 3 {} 2 3 1
+ 2 2 3 {} 5 6 4
+ 2 2 3 {} 8 9 7
+}
+
+do_execsql_test 2.5 {
+ CREATE TABLE x1(a, b, c);
+ WITH s(i) AS (
+ SELECT 1 UNION ALL SELECT i+1 FROM s WHERE i<100
+ )
+ INSERT INTO x1 SELECT i, i, hex(randomblob(500)) FROM s;
+ DROP TABLE x1;
+}
+do_recover_test 2.5.1 {
+ SELECT name FROM sqlite_master;
+ SELECT * FROM lost_and_found_1;
+} {lost_and_found lost_and_found_0 lost_and_found_1
+ 2 2 3 {} 2 3 1
+ 2 2 3 {} 5 6 4
+ 2 2 3 {} 8 9 7
+}
+
+ifcapable !secure_delete {
+ do_test 2.6 {
+ forcedelete test.db2
+ set R [sqlite3_recover_init db main test.db2]
+ $R config lostandfound lost_and_found
+ $R config freelistcorrupt 1
+ $R run
+ $R finish
+ sqlite3 db2 test.db2
+ execsql { SELECT count(*) FROM lost_and_found_1; } db2
+ } {103}
+ db2 close
+}
+
+#-------------------------------------------------------------------------
+breakpoint
+reset_db
+do_recover_test 3.0
+
+finish_test
diff --git a/ext/recover/recoverpgsz.test b/ext/recover/recoverpgsz.test
new file mode 100644
index 0000000..1a91f08
--- /dev/null
+++ b/ext/recover/recoverpgsz.test
@@ -0,0 +1,100 @@
+# 2022 October 14
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+
+source [file join [file dirname [info script]] recover_common.tcl]
+
+db close
+sqlite3_test_control_pending_byte 0x1000000
+
+set testprefix recoverpgsz
+
+foreach {pgsz bOverflow} {
+ 512 0 1024 0 2048 0 4096 0 8192 0 16384 0 32768 0 65536 0
+ 512 1 1024 1 2048 1 4096 1 8192 1 16384 1 32768 1 65536 1
+} {
+ reset_db
+ execsql "PRAGMA page_size = $pgsz"
+ execsql "PRAGMA auto_vacuum = 0"
+ do_execsql_test 1.$pgsz.$bOverflow.1 {
+ CREATE TABLE t1(a, b, c);
+ CREATE INDEX i1 ON t1(b, a, c);
+ INSERT INTO t1(a, b) VALUES(1, 2), (3, 4), (5, 6);
+ DELETE FROM t1 WHERE a=3;
+ }
+ if {$bOverflow} {
+ do_execsql_test 1.$pgsz.$bOverflow.1a {
+ UPDATE t1 SET c = randomblob(100000);
+ }
+ }
+ db close
+
+
+ set fd [open test.db]
+ fconfigure $fd -encoding binary -translation binary
+ seek $fd $pgsz
+ set pg1 [read $fd $pgsz]
+ set pg2 [read $fd $pgsz]
+ close $fd
+
+ set fd2 [open test.db2 w]
+ fconfigure $fd2 -encoding binary -translation binary
+ seek $fd2 $pgsz
+ puts -nonewline $fd2 $pg1
+ close $fd2
+
+ sqlite3 db2 test.db2
+ do_test 1.$pgsz.$bOverflow.2 {
+ set R [sqlite3_recover_init db2 main test.db3]
+ $R run
+ $R finish
+ } {}
+
+ sqlite3 db3 test.db3
+ do_test 1.$pgsz.$bOverflow.3 {
+ db3 eval { SELECT * FROM sqlite_schema }
+ db3 eval { PRAGMA page_size }
+ } $pgsz
+
+ db2 close
+ db3 close
+
+ forcedelete test.db3
+ forcedelete test.db2
+
+ set fd2 [open test.db2 w]
+ fconfigure $fd2 -encoding binary -translation binary
+ seek $fd2 $pgsz
+ puts -nonewline $fd2 $pg2
+ close $fd2
+
+ sqlite3 db2 test.db2
+ do_test 1.$pgsz.$bOverflow.4 {
+ set R [sqlite3_recover_init db2 main test.db3]
+ $R run
+ $R finish
+ } {}
+
+ sqlite3 db3 test.db3
+ do_test 1.$pgsz.$bOverflow.5 {
+ db3 eval { SELECT * FROM sqlite_schema }
+ db3 eval { PRAGMA page_size }
+ } $pgsz
+
+ db2 close
+ db3 close
+}
+
+
+finish_test
+
+
+
diff --git a/ext/recover/recoverrowid.test b/ext/recover/recoverrowid.test
new file mode 100644
index 0000000..5855e84
--- /dev/null
+++ b/ext/recover/recoverrowid.test
@@ -0,0 +1,50 @@
+# 2022 September 07
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+# Tests for the SQLITE_RECOVER_ROWIDS option.
+#
+
+source [file join [file dirname [info script]] recover_common.tcl]
+set testprefix recoverrowid
+
+proc recover {db bRowids output} {
+ forcedelete $output
+
+ set R [sqlite3_recover_init db main test.db2]
+ $R config rowids $bRowids
+ $R run
+ $R finish
+}
+
+do_execsql_test 1.0 {
+ CREATE TABLE t1(a, b);
+ INSERT INTO t1 VALUES(1, 1), (2, 2), (3, 3), (4, 4);
+ DELETE FROM t1 WHERE a IN (1, 3);
+}
+
+do_test 1.1 {
+ recover db 0 test.db2
+ sqlite3 db2 test.db2
+ execsql { SELECT rowid, a, b FROM t1 ORDER BY rowid} db2
+} {1 2 2 2 4 4}
+
+do_test 1.2 {
+ db2 close
+ recover db 1 test.db2
+ sqlite3 db2 test.db2
+ execsql { SELECT rowid, a, b FROM t1 ORDER BY rowid} db2
+} {2 2 2 4 4 4}
+db2 close
+
+
+
+
+finish_test
diff --git a/ext/recover/recoverslowidx.test b/ext/recover/recoverslowidx.test
new file mode 100644
index 0000000..2691051
--- /dev/null
+++ b/ext/recover/recoverslowidx.test
@@ -0,0 +1,87 @@
+# 2022 September 25
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+# Tests for the SQLITE_RECOVER_SLOWINDEXES option.
+#
+
+source [file join [file dirname [info script]] recover_common.tcl]
+set testprefix recoverslowidx
+
+do_execsql_test 1.0 {
+ PRAGMA auto_vacuum = 0;
+ CREATE TABLE t1(a, b);
+ CREATE INDEX i1 ON t1(a);
+ INSERT INTO t1 VALUES(1, 1), (2, 2), (3, 3), (4, 4);
+}
+
+proc my_sql_hook {sql} {
+ lappend ::lSql $sql
+ return 0
+}
+
+do_test 1.1 {
+ set lSql [list]
+ set R [sqlite3_recover_init_sql db main my_sql_hook]
+ while {[$R step]==0} { }
+ $R finish
+} {}
+
+do_test 1.2 {
+ set lSql
+} [list {*}{
+ {BEGIN}
+ {PRAGMA writable_schema = on}
+ {PRAGMA encoding = 'UTF-8'}
+ {PRAGMA page_size = '1024'}
+ {PRAGMA auto_vacuum = '0'}
+ {PRAGMA user_version = '0'}
+ {PRAGMA application_id = '0'}
+ {CREATE TABLE t1(a, b)}
+ {INSERT OR IGNORE INTO 't1'(_rowid_, 'a', 'b') VALUES (1, 1, 1)}
+ {INSERT OR IGNORE INTO 't1'(_rowid_, 'a', 'b') VALUES (2, 2, 2)}
+ {INSERT OR IGNORE INTO 't1'(_rowid_, 'a', 'b') VALUES (3, 3, 3)}
+ {INSERT OR IGNORE INTO 't1'(_rowid_, 'a', 'b') VALUES (4, 4, 4)}
+ {CREATE INDEX i1 ON t1(a)}
+ {PRAGMA writable_schema = off}
+ {COMMIT}
+}]
+
+do_test 1.3 {
+ set lSql [list]
+ set R [sqlite3_recover_init_sql db main my_sql_hook]
+ $R config slowindexes 1
+ while {[$R step]==0} { }
+ $R finish
+} {}
+
+do_test 1.4 {
+ set lSql
+} [list {*}{
+ {BEGIN}
+ {PRAGMA writable_schema = on}
+ {PRAGMA encoding = 'UTF-8'}
+ {PRAGMA page_size = '1024'}
+ {PRAGMA auto_vacuum = '0'}
+ {PRAGMA user_version = '0'}
+ {PRAGMA application_id = '0'}
+ {CREATE TABLE t1(a, b)}
+ {CREATE INDEX i1 ON t1(a)}
+ {INSERT OR IGNORE INTO 't1'(_rowid_, 'a', 'b') VALUES (1, 1, 1)}
+ {INSERT OR IGNORE INTO 't1'(_rowid_, 'a', 'b') VALUES (2, 2, 2)}
+ {INSERT OR IGNORE INTO 't1'(_rowid_, 'a', 'b') VALUES (3, 3, 3)}
+ {INSERT OR IGNORE INTO 't1'(_rowid_, 'a', 'b') VALUES (4, 4, 4)}
+ {PRAGMA writable_schema = off}
+ {COMMIT}
+}]
+
+
+finish_test
+
diff --git a/ext/recover/recoversql.test b/ext/recover/recoversql.test
new file mode 100644
index 0000000..0a67267
--- /dev/null
+++ b/ext/recover/recoversql.test
@@ -0,0 +1,52 @@
+# 2022 September 13
+#
+# The author disclaims copyright to this source code. In place of
+# a legal notice, here is a blessing:
+#
+# May you do good and not evil.
+# May you find forgiveness for yourself and forgive others.
+# May you share freely, never taking more than you give.
+#
+#***********************************************************************
+#
+#
+
+source [file join [file dirname [info script]] recover_common.tcl]
+set testprefix recoversql
+
+do_execsql_test 1.0 {
+ CREATE TABLE "x.1" (x, y);
+ INSERT INTO "x.1" VALUES(1, 1), (2, 2), (3, 3);
+ CREATE INDEX "i.1" ON "x.1"(y, x);
+}
+
+proc sql_hook {sql} {
+ incr ::iSqlHook
+ if {$::iSqlHook==$::sql_hook_cnt} { return 4 }
+ return 0
+}
+
+do_test 1.1 {
+ set ::sql_hook_cnt -1
+ set ::iSqlHook 0
+ set R [sqlite3_recover_init_sql db main sql_hook]
+ $R run
+ $R finish
+} {}
+
+set nSqlCall $iSqlHook
+
+for {set ii 1} {$ii<$nSqlCall} {incr ii} {
+ set iSqlHook 0
+ set sql_hook_cnt $ii
+ do_test 1.$ii.a {
+ set R [sqlite3_recover_init_sql db main sql_hook]
+ $R run
+ } {1}
+ do_test 1.$ii.b {
+ list [catch { $R finish } msg] $msg
+ } {1 {callback returned an error - 4}}
+}
+
+
+finish_test
diff --git a/ext/recover/sqlite3recover.c b/ext/recover/sqlite3recover.c
new file mode 100644
index 0000000..e62aba7
--- /dev/null
+++ b/ext/recover/sqlite3recover.c
@@ -0,0 +1,2861 @@
+/*
+** 2022-08-27
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+**
+*/
+
+
+#include "sqlite3recover.h"
+#include <assert.h>
+#include <string.h>
+
+#ifndef SQLITE_OMIT_VIRTUALTABLE
+
+/*
+** Declaration for public API function in file dbdata.c. This may be called
+** with NULL as the final two arguments to register the sqlite_dbptr and
+** sqlite_dbdata virtual tables with a database handle.
+*/
+#ifdef _WIN32
+__declspec(dllexport)
+#endif
+int sqlite3_dbdata_init(sqlite3*, char**, const sqlite3_api_routines*);
+
+typedef unsigned int u32;
+typedef unsigned char u8;
+typedef sqlite3_int64 i64;
+
+typedef struct RecoverTable RecoverTable;
+typedef struct RecoverColumn RecoverColumn;
+
+/*
+** When recovering rows of data that can be associated with table
+** definitions recovered from the sqlite_schema table, each table is
+** represented by an instance of the following object.
+**
+** iRoot:
+** The root page in the original database. Not necessarily (and usually
+** not) the same in the recovered database.
+**
+** zTab:
+** Name of the table.
+**
+** nCol/aCol[]:
+** aCol[] is an array of nCol columns. In the order in which they appear
+** in the table.
+**
+** bIntkey:
+** Set to true for intkey tables, false for WITHOUT ROWID.
+**
+** iRowidBind:
+** Each column in the aCol[] array has associated with it the index of
+** the bind parameter its values will be bound to in the INSERT statement
+** used to construct the output database. If the table does has a rowid
+** but not an INTEGER PRIMARY KEY column, then iRowidBind contains the
+** index of the bind paramater to which the rowid value should be bound.
+** Otherwise, it contains -1. If the table does contain an INTEGER PRIMARY
+** KEY column, then the rowid value should be bound to the index associated
+** with the column.
+**
+** pNext:
+** All RecoverTable objects used by the recovery operation are allocated
+** and populated as part of creating the recovered database schema in
+** the output database, before any non-schema data are recovered. They
+** are then stored in a singly-linked list linked by this variable beginning
+** at sqlite3_recover.pTblList.
+*/
+struct RecoverTable {
+ u32 iRoot; /* Root page in original database */
+ char *zTab; /* Name of table */
+ int nCol; /* Number of columns in table */
+ RecoverColumn *aCol; /* Array of columns */
+ int bIntkey; /* True for intkey, false for without rowid */
+ int iRowidBind; /* If >0, bind rowid to INSERT here */
+ RecoverTable *pNext;
+};
+
+/*
+** Each database column is represented by an instance of the following object
+** stored in the RecoverTable.aCol[] array of the associated table.
+**
+** iField:
+** The index of the associated field within database records. Or -1 if
+** there is no associated field (e.g. for virtual generated columns).
+**
+** iBind:
+** The bind index of the INSERT statement to bind this columns values
+** to. Or 0 if there is no such index (iff (iField<0)).
+**
+** bIPK:
+** True if this is the INTEGER PRIMARY KEY column.
+**
+** zCol:
+** Name of column.
+**
+** eHidden:
+** A RECOVER_EHIDDEN_* constant value (see below for interpretation of each).
+*/
+struct RecoverColumn {
+ int iField; /* Field in record on disk */
+ int iBind; /* Binding to use in INSERT */
+ int bIPK; /* True for IPK column */
+ char *zCol;
+ int eHidden;
+};
+
+#define RECOVER_EHIDDEN_NONE 0 /* Normal database column */
+#define RECOVER_EHIDDEN_HIDDEN 1 /* Column is __HIDDEN__ */
+#define RECOVER_EHIDDEN_VIRTUAL 2 /* Virtual generated column */
+#define RECOVER_EHIDDEN_STORED 3 /* Stored generated column */
+
+/*
+** Bitmap object used to track pages in the input database. Allocated
+** and manipulated only by the following functions:
+**
+** recoverBitmapAlloc()
+** recoverBitmapFree()
+** recoverBitmapSet()
+** recoverBitmapQuery()
+**
+** nPg:
+** Largest page number that may be stored in the bitmap. The range
+** of valid keys is 1 to nPg, inclusive.
+**
+** aElem[]:
+** Array large enough to contain a bit for each key. For key value
+** iKey, the associated bit is the bit (iKey%32) of aElem[iKey/32].
+** In other words, the following is true if bit iKey is set, or
+** false if it is clear:
+**
+** (aElem[iKey/32] & (1 << (iKey%32))) ? 1 : 0
+*/
+typedef struct RecoverBitmap RecoverBitmap;
+struct RecoverBitmap {
+ i64 nPg; /* Size of bitmap */
+ u32 aElem[1]; /* Array of 32-bit bitmasks */
+};
+
+/*
+** State variables (part of the sqlite3_recover structure) used while
+** recovering data for tables identified in the recovered schema (state
+** RECOVER_STATE_WRITING).
+*/
+typedef struct RecoverStateW1 RecoverStateW1;
+struct RecoverStateW1 {
+ sqlite3_stmt *pTbls;
+ sqlite3_stmt *pSel;
+ sqlite3_stmt *pInsert;
+ int nInsert;
+
+ RecoverTable *pTab; /* Table currently being written */
+ int nMax; /* Max column count in any schema table */
+ sqlite3_value **apVal; /* Array of nMax values */
+ int nVal; /* Number of valid entries in apVal[] */
+ int bHaveRowid;
+ i64 iRowid;
+ i64 iPrevPage;
+ int iPrevCell;
+};
+
+/*
+** State variables (part of the sqlite3_recover structure) used while
+** recovering data destined for the lost and found table (states
+** RECOVER_STATE_LOSTANDFOUND[123]).
+*/
+typedef struct RecoverStateLAF RecoverStateLAF;
+struct RecoverStateLAF {
+ RecoverBitmap *pUsed;
+ i64 nPg; /* Size of db in pages */
+ sqlite3_stmt *pAllAndParent;
+ sqlite3_stmt *pMapInsert;
+ sqlite3_stmt *pMaxField;
+ sqlite3_stmt *pUsedPages;
+ sqlite3_stmt *pFindRoot;
+ sqlite3_stmt *pInsert; /* INSERT INTO lost_and_found ... */
+ sqlite3_stmt *pAllPage;
+ sqlite3_stmt *pPageData;
+ sqlite3_value **apVal;
+ int nMaxField;
+};
+
+/*
+** Main recover handle structure.
+*/
+struct sqlite3_recover {
+ /* Copies of sqlite3_recover_init[_sql]() parameters */
+ sqlite3 *dbIn; /* Input database */
+ char *zDb; /* Name of input db ("main" etc.) */
+ char *zUri; /* URI for output database */
+ void *pSqlCtx; /* SQL callback context */
+ int (*xSql)(void*,const char*); /* Pointer to SQL callback function */
+
+ /* Values configured by sqlite3_recover_config() */
+ char *zStateDb; /* State database to use (or NULL) */
+ char *zLostAndFound; /* Name of lost-and-found table (or NULL) */
+ int bFreelistCorrupt; /* SQLITE_RECOVER_FREELIST_CORRUPT setting */
+ int bRecoverRowid; /* SQLITE_RECOVER_ROWIDS setting */
+ int bSlowIndexes; /* SQLITE_RECOVER_SLOWINDEXES setting */
+
+ int pgsz;
+ int detected_pgsz;
+ int nReserve;
+ u8 *pPage1Disk;
+ u8 *pPage1Cache;
+
+ /* Error code and error message */
+ int errCode; /* For sqlite3_recover_errcode() */
+ char *zErrMsg; /* For sqlite3_recover_errmsg() */
+
+ int eState;
+ int bCloseTransaction;
+
+ /* Variables used with eState==RECOVER_STATE_WRITING */
+ RecoverStateW1 w1;
+
+ /* Variables used with states RECOVER_STATE_LOSTANDFOUND[123] */
+ RecoverStateLAF laf;
+
+ /* Fields used within sqlite3_recover_run() */
+ sqlite3 *dbOut; /* Output database */
+ sqlite3_stmt *pGetPage; /* SELECT against input db sqlite_dbdata */
+ RecoverTable *pTblList; /* List of tables recovered from schema */
+};
+
+/*
+** The various states in which an sqlite3_recover object may exist:
+**
+** RECOVER_STATE_INIT:
+** The object is initially created in this state. sqlite3_recover_step()
+** has yet to be called. This is the only state in which it is permitted
+** to call sqlite3_recover_config().
+**
+** RECOVER_STATE_WRITING:
+**
+** RECOVER_STATE_LOSTANDFOUND1:
+** State to populate the bitmap of pages used by other tables or the
+** database freelist.
+**
+** RECOVER_STATE_LOSTANDFOUND2:
+** Populate the recovery.map table - used to figure out a "root" page
+** for each lost page from in the database from which records are
+** extracted.
+**
+** RECOVER_STATE_LOSTANDFOUND3:
+** Populate the lost-and-found table itself.
+*/
+#define RECOVER_STATE_INIT 0
+#define RECOVER_STATE_WRITING 1
+#define RECOVER_STATE_LOSTANDFOUND1 2
+#define RECOVER_STATE_LOSTANDFOUND2 3
+#define RECOVER_STATE_LOSTANDFOUND3 4
+#define RECOVER_STATE_SCHEMA2 5
+#define RECOVER_STATE_DONE 6
+
+
+/*
+** Global variables used by this extension.
+*/
+typedef struct RecoverGlobal RecoverGlobal;
+struct RecoverGlobal {
+ const sqlite3_io_methods *pMethods;
+ sqlite3_recover *p;
+};
+static RecoverGlobal recover_g;
+
+/*
+** Use this static SQLite mutex to protect the globals during the
+** first call to sqlite3_recover_step().
+*/
+#define RECOVER_MUTEX_ID SQLITE_MUTEX_STATIC_APP2
+
+
+/*
+** Default value for SQLITE_RECOVER_ROWIDS (sqlite3_recover.bRecoverRowid).
+*/
+#define RECOVER_ROWID_DEFAULT 1
+
+/*
+** Mutex handling:
+**
+** recoverEnterMutex() - Enter the recovery mutex
+** recoverLeaveMutex() - Leave the recovery mutex
+** recoverAssertMutexHeld() - Assert that the recovery mutex is held
+*/
+#if defined(SQLITE_THREADSAFE) && SQLITE_THREADSAFE==0
+# define recoverEnterMutex()
+# define recoverLeaveMutex()
+#else
+static void recoverEnterMutex(void){
+ sqlite3_mutex_enter(sqlite3_mutex_alloc(RECOVER_MUTEX_ID));
+}
+static void recoverLeaveMutex(void){
+ sqlite3_mutex_leave(sqlite3_mutex_alloc(RECOVER_MUTEX_ID));
+}
+#endif
+#if SQLITE_THREADSAFE+0>=1 && defined(SQLITE_DEBUG)
+static void recoverAssertMutexHeld(void){
+ assert( sqlite3_mutex_held(sqlite3_mutex_alloc(RECOVER_MUTEX_ID)) );
+}
+#else
+# define recoverAssertMutexHeld()
+#endif
+
+
+/*
+** Like strlen(). But handles NULL pointer arguments.
+*/
+static int recoverStrlen(const char *zStr){
+ if( zStr==0 ) return 0;
+ return (int)(strlen(zStr)&0x7fffffff);
+}
+
+/*
+** This function is a no-op if the recover handle passed as the first
+** argument already contains an error (if p->errCode!=SQLITE_OK).
+**
+** Otherwise, an attempt is made to allocate, zero and return a buffer nByte
+** bytes in size. If successful, a pointer to the new buffer is returned. Or,
+** if an OOM error occurs, NULL is returned and the handle error code
+** (p->errCode) set to SQLITE_NOMEM.
+*/
+static void *recoverMalloc(sqlite3_recover *p, i64 nByte){
+ void *pRet = 0;
+ assert( nByte>0 );
+ if( p->errCode==SQLITE_OK ){
+ pRet = sqlite3_malloc64(nByte);
+ if( pRet ){
+ memset(pRet, 0, nByte);
+ }else{
+ p->errCode = SQLITE_NOMEM;
+ }
+ }
+ return pRet;
+}
+
+/*
+** Set the error code and error message for the recover handle passed as
+** the first argument. The error code is set to the value of parameter
+** errCode.
+**
+** Parameter zFmt must be a printf() style formatting string. The handle
+** error message is set to the result of using any trailing arguments for
+** parameter substitutions in the formatting string.
+**
+** For example:
+**
+** recoverError(p, SQLITE_ERROR, "no such table: %s", zTablename);
+*/
+static int recoverError(
+ sqlite3_recover *p,
+ int errCode,
+ const char *zFmt, ...
+){
+ char *z = 0;
+ va_list ap;
+ va_start(ap, zFmt);
+ if( zFmt ){
+ z = sqlite3_vmprintf(zFmt, ap);
+ va_end(ap);
+ }
+ sqlite3_free(p->zErrMsg);
+ p->zErrMsg = z;
+ p->errCode = errCode;
+ return errCode;
+}
+
+
+/*
+** This function is a no-op if p->errCode is initially other than SQLITE_OK.
+** In this case it returns NULL.
+**
+** Otherwise, an attempt is made to allocate and return a bitmap object
+** large enough to store a bit for all page numbers between 1 and nPg,
+** inclusive. The bitmap is initially zeroed.
+*/
+static RecoverBitmap *recoverBitmapAlloc(sqlite3_recover *p, i64 nPg){
+ int nElem = (nPg+1+31) / 32;
+ int nByte = sizeof(RecoverBitmap) + nElem*sizeof(u32);
+ RecoverBitmap *pRet = (RecoverBitmap*)recoverMalloc(p, nByte);
+
+ if( pRet ){
+ pRet->nPg = nPg;
+ }
+ return pRet;
+}
+
+/*
+** Free a bitmap object allocated by recoverBitmapAlloc().
+*/
+static void recoverBitmapFree(RecoverBitmap *pMap){
+ sqlite3_free(pMap);
+}
+
+/*
+** Set the bit associated with page iPg in bitvec pMap.
+*/
+static void recoverBitmapSet(RecoverBitmap *pMap, i64 iPg){
+ if( iPg<=pMap->nPg ){
+ int iElem = (iPg / 32);
+ int iBit = (iPg % 32);
+ pMap->aElem[iElem] |= (((u32)1) << iBit);
+ }
+}
+
+/*
+** Query bitmap object pMap for the state of the bit associated with page
+** iPg. Return 1 if it is set, or 0 otherwise.
+*/
+static int recoverBitmapQuery(RecoverBitmap *pMap, i64 iPg){
+ int ret = 1;
+ if( iPg<=pMap->nPg && iPg>0 ){
+ int iElem = (iPg / 32);
+ int iBit = (iPg % 32);
+ ret = (pMap->aElem[iElem] & (((u32)1) << iBit)) ? 1 : 0;
+ }
+ return ret;
+}
+
+/*
+** Set the recover handle error to the error code and message returned by
+** calling sqlite3_errcode() and sqlite3_errmsg(), respectively, on database
+** handle db.
+*/
+static int recoverDbError(sqlite3_recover *p, sqlite3 *db){
+ return recoverError(p, sqlite3_errcode(db), "%s", sqlite3_errmsg(db));
+}
+
+/*
+** This function is a no-op if recover handle p already contains an error
+** (if p->errCode!=SQLITE_OK).
+**
+** Otherwise, it attempts to prepare the SQL statement in zSql against
+** database handle db. If successful, the statement handle is returned.
+** Or, if an error occurs, NULL is returned and an error left in the
+** recover handle.
+*/
+static sqlite3_stmt *recoverPrepare(
+ sqlite3_recover *p,
+ sqlite3 *db,
+ const char *zSql
+){
+ sqlite3_stmt *pStmt = 0;
+ if( p->errCode==SQLITE_OK ){
+ if( sqlite3_prepare_v2(db, zSql, -1, &pStmt, 0) ){
+ recoverDbError(p, db);
+ }
+ }
+ return pStmt;
+}
+
+/*
+** This function is a no-op if recover handle p already contains an error
+** (if p->errCode!=SQLITE_OK).
+**
+** Otherwise, argument zFmt is used as a printf() style format string,
+** along with any trailing arguments, to create an SQL statement. This
+** SQL statement is prepared against database handle db and, if successful,
+** the statment handle returned. Or, if an error occurs - either during
+** the printf() formatting or when preparing the resulting SQL - an
+** error code and message are left in the recover handle.
+*/
+static sqlite3_stmt *recoverPreparePrintf(
+ sqlite3_recover *p,
+ sqlite3 *db,
+ const char *zFmt, ...
+){
+ sqlite3_stmt *pStmt = 0;
+ if( p->errCode==SQLITE_OK ){
+ va_list ap;
+ char *z;
+ va_start(ap, zFmt);
+ z = sqlite3_vmprintf(zFmt, ap);
+ va_end(ap);
+ if( z==0 ){
+ p->errCode = SQLITE_NOMEM;
+ }else{
+ pStmt = recoverPrepare(p, db, z);
+ sqlite3_free(z);
+ }
+ }
+ return pStmt;
+}
+
+/*
+** Reset SQLite statement handle pStmt. If the call to sqlite3_reset()
+** indicates that an error occurred, and there is not already an error
+** in the recover handle passed as the first argument, set the error
+** code and error message appropriately.
+**
+** This function returns a copy of the statement handle pointer passed
+** as the second argument.
+*/
+static sqlite3_stmt *recoverReset(sqlite3_recover *p, sqlite3_stmt *pStmt){
+ int rc = sqlite3_reset(pStmt);
+ if( rc!=SQLITE_OK && rc!=SQLITE_CONSTRAINT && p->errCode==SQLITE_OK ){
+ recoverDbError(p, sqlite3_db_handle(pStmt));
+ }
+ return pStmt;
+}
+
+/*
+** Finalize SQLite statement handle pStmt. If the call to sqlite3_reset()
+** indicates that an error occurred, and there is not already an error
+** in the recover handle passed as the first argument, set the error
+** code and error message appropriately.
+*/
+static void recoverFinalize(sqlite3_recover *p, sqlite3_stmt *pStmt){
+ sqlite3 *db = sqlite3_db_handle(pStmt);
+ int rc = sqlite3_finalize(pStmt);
+ if( rc!=SQLITE_OK && p->errCode==SQLITE_OK ){
+ recoverDbError(p, db);
+ }
+}
+
+/*
+** This function is a no-op if recover handle p already contains an error
+** (if p->errCode!=SQLITE_OK). A copy of p->errCode is returned in this
+** case.
+**
+** Otherwise, execute SQL script zSql. If successful, return SQLITE_OK.
+** Or, if an error occurs, leave an error code and message in the recover
+** handle and return a copy of the error code.
+*/
+static int recoverExec(sqlite3_recover *p, sqlite3 *db, const char *zSql){
+ if( p->errCode==SQLITE_OK ){
+ int rc = sqlite3_exec(db, zSql, 0, 0, 0);
+ if( rc ){
+ recoverDbError(p, db);
+ }
+ }
+ return p->errCode;
+}
+
+/*
+** Bind the value pVal to parameter iBind of statement pStmt. Leave an
+** error in the recover handle passed as the first argument if an error
+** (e.g. an OOM) occurs.
+*/
+static void recoverBindValue(
+ sqlite3_recover *p,
+ sqlite3_stmt *pStmt,
+ int iBind,
+ sqlite3_value *pVal
+){
+ if( p->errCode==SQLITE_OK ){
+ int rc = sqlite3_bind_value(pStmt, iBind, pVal);
+ if( rc ) recoverError(p, rc, 0);
+ }
+}
+
+/*
+** This function is a no-op if recover handle p already contains an error
+** (if p->errCode!=SQLITE_OK). NULL is returned in this case.
+**
+** Otherwise, an attempt is made to interpret zFmt as a printf() style
+** formatting string and the result of using the trailing arguments for
+** parameter substitution with it written into a buffer obtained from
+** sqlite3_malloc(). If successful, a pointer to the buffer is returned.
+** It is the responsibility of the caller to eventually free the buffer
+** using sqlite3_free().
+**
+** Or, if an error occurs, an error code and message is left in the recover
+** handle and NULL returned.
+*/
+static char *recoverMPrintf(sqlite3_recover *p, const char *zFmt, ...){
+ va_list ap;
+ char *z;
+ va_start(ap, zFmt);
+ z = sqlite3_vmprintf(zFmt, ap);
+ va_end(ap);
+ if( p->errCode==SQLITE_OK ){
+ if( z==0 ) p->errCode = SQLITE_NOMEM;
+ }else{
+ sqlite3_free(z);
+ z = 0;
+ }
+ return z;
+}
+
+/*
+** This function is a no-op if recover handle p already contains an error
+** (if p->errCode!=SQLITE_OK). Zero is returned in this case.
+**
+** Otherwise, execute "PRAGMA page_count" against the input database. If
+** successful, return the integer result. Or, if an error occurs, leave an
+** error code and error message in the sqlite3_recover handle and return
+** zero.
+*/
+static i64 recoverPageCount(sqlite3_recover *p){
+ i64 nPg = 0;
+ if( p->errCode==SQLITE_OK ){
+ sqlite3_stmt *pStmt = 0;
+ pStmt = recoverPreparePrintf(p, p->dbIn, "PRAGMA %Q.page_count", p->zDb);
+ if( pStmt ){
+ sqlite3_step(pStmt);
+ nPg = sqlite3_column_int64(pStmt, 0);
+ }
+ recoverFinalize(p, pStmt);
+ }
+ return nPg;
+}
+
+/*
+** Implementation of SQL scalar function "read_i32". The first argument to
+** this function must be a blob. The second a non-negative integer. This
+** function reads and returns a 32-bit big-endian integer from byte
+** offset (4*<arg2>) of the blob.
+**
+** SELECT read_i32(<blob>, <idx>)
+*/
+static void recoverReadI32(
+ sqlite3_context *context,
+ int argc,
+ sqlite3_value **argv
+){
+ const unsigned char *pBlob;
+ int nBlob;
+ int iInt;
+
+ assert( argc==2 );
+ nBlob = sqlite3_value_bytes(argv[0]);
+ pBlob = (const unsigned char*)sqlite3_value_blob(argv[0]);
+ iInt = sqlite3_value_int(argv[1]) & 0xFFFF;
+
+ if( (iInt+1)*4<=nBlob ){
+ const unsigned char *a = &pBlob[iInt*4];
+ i64 iVal = ((i64)a[0]<<24)
+ + ((i64)a[1]<<16)
+ + ((i64)a[2]<< 8)
+ + ((i64)a[3]<< 0);
+ sqlite3_result_int64(context, iVal);
+ }
+}
+
+/*
+** Implementation of SQL scalar function "page_is_used". This function
+** is used as part of the procedure for locating orphan rows for the
+** lost-and-found table, and it depends on those routines having populated
+** the sqlite3_recover.laf.pUsed variable.
+**
+** The only argument to this function is a page-number. It returns true
+** if the page has already been used somehow during data recovery, or false
+** otherwise.
+**
+** SELECT page_is_used(<pgno>);
+*/
+static void recoverPageIsUsed(
+ sqlite3_context *pCtx,
+ int nArg,
+ sqlite3_value **apArg
+){
+ sqlite3_recover *p = (sqlite3_recover*)sqlite3_user_data(pCtx);
+ i64 pgno = sqlite3_value_int64(apArg[0]);
+ assert( nArg==1 );
+ sqlite3_result_int(pCtx, recoverBitmapQuery(p->laf.pUsed, pgno));
+}
+
+/*
+** The implementation of a user-defined SQL function invoked by the
+** sqlite_dbdata and sqlite_dbptr virtual table modules to access pages
+** of the database being recovered.
+**
+** This function always takes a single integer argument. If the argument
+** is zero, then the value returned is the number of pages in the db being
+** recovered. If the argument is greater than zero, it is a page number.
+** The value returned in this case is an SQL blob containing the data for
+** the identified page of the db being recovered. e.g.
+**
+** SELECT getpage(0); -- return number of pages in db
+** SELECT getpage(4); -- return page 4 of db as a blob of data
+*/
+static void recoverGetPage(
+ sqlite3_context *pCtx,
+ int nArg,
+ sqlite3_value **apArg
+){
+ sqlite3_recover *p = (sqlite3_recover*)sqlite3_user_data(pCtx);
+ i64 pgno = sqlite3_value_int64(apArg[0]);
+ sqlite3_stmt *pStmt = 0;
+
+ assert( nArg==1 );
+ if( pgno==0 ){
+ i64 nPg = recoverPageCount(p);
+ sqlite3_result_int64(pCtx, nPg);
+ return;
+ }else{
+ if( p->pGetPage==0 ){
+ pStmt = p->pGetPage = recoverPreparePrintf(
+ p, p->dbIn, "SELECT data FROM sqlite_dbpage(%Q) WHERE pgno=?", p->zDb
+ );
+ }else if( p->errCode==SQLITE_OK ){
+ pStmt = p->pGetPage;
+ }
+
+ if( pStmt ){
+ sqlite3_bind_int64(pStmt, 1, pgno);
+ if( SQLITE_ROW==sqlite3_step(pStmt) ){
+ const u8 *aPg;
+ int nPg;
+ assert( p->errCode==SQLITE_OK );
+ aPg = sqlite3_column_blob(pStmt, 0);
+ nPg = sqlite3_column_bytes(pStmt, 0);
+ if( pgno==1 && nPg==p->pgsz && 0==memcmp(p->pPage1Cache, aPg, nPg) ){
+ aPg = p->pPage1Disk;
+ }
+ sqlite3_result_blob(pCtx, aPg, nPg-p->nReserve, SQLITE_TRANSIENT);
+ }
+ recoverReset(p, pStmt);
+ }
+ }
+
+ if( p->errCode ){
+ if( p->zErrMsg ) sqlite3_result_error(pCtx, p->zErrMsg, -1);
+ sqlite3_result_error_code(pCtx, p->errCode);
+ }
+}
+
+/*
+** Find a string that is not found anywhere in z[]. Return a pointer
+** to that string.
+**
+** Try to use zA and zB first. If both of those are already found in z[]
+** then make up some string and store it in the buffer zBuf.
+*/
+static const char *recoverUnusedString(
+ const char *z, /* Result must not appear anywhere in z */
+ const char *zA, const char *zB, /* Try these first */
+ char *zBuf /* Space to store a generated string */
+){
+ unsigned i = 0;
+ if( strstr(z, zA)==0 ) return zA;
+ if( strstr(z, zB)==0 ) return zB;
+ do{
+ sqlite3_snprintf(20,zBuf,"(%s%u)", zA, i++);
+ }while( strstr(z,zBuf)!=0 );
+ return zBuf;
+}
+
+/*
+** Implementation of scalar SQL function "escape_crnl". The argument passed to
+** this function is the output of built-in function quote(). If the first
+** character of the input is "'", indicating that the value passed to quote()
+** was a text value, then this function searches the input for "\n" and "\r"
+** characters and adds a wrapper similar to the following:
+**
+** replace(replace(<input>, '\n', char(10), '\r', char(13));
+**
+** Or, if the first character of the input is not "'", then a copy of the input
+** is returned.
+*/
+static void recoverEscapeCrnl(
+ sqlite3_context *context,
+ int argc,
+ sqlite3_value **argv
+){
+ const char *zText = (const char*)sqlite3_value_text(argv[0]);
+ if( zText && zText[0]=='\'' ){
+ int nText = sqlite3_value_bytes(argv[0]);
+ int i;
+ char zBuf1[20];
+ char zBuf2[20];
+ const char *zNL = 0;
+ const char *zCR = 0;
+ int nCR = 0;
+ int nNL = 0;
+
+ for(i=0; zText[i]; i++){
+ if( zNL==0 && zText[i]=='\n' ){
+ zNL = recoverUnusedString(zText, "\\n", "\\012", zBuf1);
+ nNL = (int)strlen(zNL);
+ }
+ if( zCR==0 && zText[i]=='\r' ){
+ zCR = recoverUnusedString(zText, "\\r", "\\015", zBuf2);
+ nCR = (int)strlen(zCR);
+ }
+ }
+
+ if( zNL || zCR ){
+ int iOut = 0;
+ i64 nMax = (nNL > nCR) ? nNL : nCR;
+ i64 nAlloc = nMax * nText + (nMax+64)*2;
+ char *zOut = (char*)sqlite3_malloc64(nAlloc);
+ if( zOut==0 ){
+ sqlite3_result_error_nomem(context);
+ return;
+ }
+
+ if( zNL && zCR ){
+ memcpy(&zOut[iOut], "replace(replace(", 16);
+ iOut += 16;
+ }else{
+ memcpy(&zOut[iOut], "replace(", 8);
+ iOut += 8;
+ }
+ for(i=0; zText[i]; i++){
+ if( zText[i]=='\n' ){
+ memcpy(&zOut[iOut], zNL, nNL);
+ iOut += nNL;
+ }else if( zText[i]=='\r' ){
+ memcpy(&zOut[iOut], zCR, nCR);
+ iOut += nCR;
+ }else{
+ zOut[iOut] = zText[i];
+ iOut++;
+ }
+ }
+
+ if( zNL ){
+ memcpy(&zOut[iOut], ",'", 2); iOut += 2;
+ memcpy(&zOut[iOut], zNL, nNL); iOut += nNL;
+ memcpy(&zOut[iOut], "', char(10))", 12); iOut += 12;
+ }
+ if( zCR ){
+ memcpy(&zOut[iOut], ",'", 2); iOut += 2;
+ memcpy(&zOut[iOut], zCR, nCR); iOut += nCR;
+ memcpy(&zOut[iOut], "', char(13))", 12); iOut += 12;
+ }
+
+ sqlite3_result_text(context, zOut, iOut, SQLITE_TRANSIENT);
+ sqlite3_free(zOut);
+ return;
+ }
+ }
+
+ sqlite3_result_value(context, argv[0]);
+}
+
+/*
+** This function is a no-op if recover handle p already contains an error
+** (if p->errCode!=SQLITE_OK). A copy of the error code is returned in
+** this case.
+**
+** Otherwise, attempt to populate temporary table "recovery.schema" with the
+** parts of the database schema that can be extracted from the input database.
+**
+** If no error occurs, SQLITE_OK is returned. Otherwise, an error code
+** and error message are left in the recover handle and a copy of the
+** error code returned. It is not considered an error if part of all of
+** the database schema cannot be recovered due to corruption.
+*/
+static int recoverCacheSchema(sqlite3_recover *p){
+ return recoverExec(p, p->dbOut,
+ "WITH RECURSIVE pages(p) AS ("
+ " SELECT 1"
+ " UNION"
+ " SELECT child FROM sqlite_dbptr('getpage()'), pages WHERE pgno=p"
+ ")"
+ "INSERT INTO recovery.schema SELECT"
+ " max(CASE WHEN field=0 THEN value ELSE NULL END),"
+ " max(CASE WHEN field=1 THEN value ELSE NULL END),"
+ " max(CASE WHEN field=2 THEN value ELSE NULL END),"
+ " max(CASE WHEN field=3 THEN value ELSE NULL END),"
+ " max(CASE WHEN field=4 THEN value ELSE NULL END)"
+ "FROM sqlite_dbdata('getpage()') WHERE pgno IN ("
+ " SELECT p FROM pages"
+ ") GROUP BY pgno, cell"
+ );
+}
+
+/*
+** If this recover handle is not in SQL callback mode (i.e. was not created
+** using sqlite3_recover_init_sql()) of if an error has already occurred,
+** this function is a no-op. Otherwise, issue a callback with SQL statement
+** zSql as the parameter.
+**
+** If the callback returns non-zero, set the recover handle error code to
+** the value returned (so that the caller will abandon processing).
+*/
+static void recoverSqlCallback(sqlite3_recover *p, const char *zSql){
+ if( p->errCode==SQLITE_OK && p->xSql ){
+ int res = p->xSql(p->pSqlCtx, zSql);
+ if( res ){
+ recoverError(p, SQLITE_ERROR, "callback returned an error - %d", res);
+ }
+ }
+}
+
+/*
+** Transfer the following settings from the input database to the output
+** database:
+**
+** + page-size,
+** + auto-vacuum settings,
+** + database encoding,
+** + user-version (PRAGMA user_version), and
+** + application-id (PRAGMA application_id), and
+*/
+static void recoverTransferSettings(sqlite3_recover *p){
+ const char *aPragma[] = {
+ "encoding",
+ "page_size",
+ "auto_vacuum",
+ "user_version",
+ "application_id"
+ };
+ int ii;
+
+ /* Truncate the output database to 0 pages in size. This is done by
+ ** opening a new, empty, temp db, then using the backup API to clobber
+ ** any existing output db with a copy of it. */
+ if( p->errCode==SQLITE_OK ){
+ sqlite3 *db2 = 0;
+ int rc = sqlite3_open("", &db2);
+ if( rc!=SQLITE_OK ){
+ recoverDbError(p, db2);
+ return;
+ }
+
+ for(ii=0; ii<sizeof(aPragma)/sizeof(aPragma[0]); ii++){
+ const char *zPrag = aPragma[ii];
+ sqlite3_stmt *p1 = 0;
+ p1 = recoverPreparePrintf(p, p->dbIn, "PRAGMA %Q.%s", p->zDb, zPrag);
+ if( p->errCode==SQLITE_OK && sqlite3_step(p1)==SQLITE_ROW ){
+ const char *zArg = (const char*)sqlite3_column_text(p1, 0);
+ char *z2 = recoverMPrintf(p, "PRAGMA %s = %Q", zPrag, zArg);
+ recoverSqlCallback(p, z2);
+ recoverExec(p, db2, z2);
+ sqlite3_free(z2);
+ if( zArg==0 ){
+ recoverError(p, SQLITE_NOMEM, 0);
+ }
+ }
+ recoverFinalize(p, p1);
+ }
+ recoverExec(p, db2, "CREATE TABLE t1(a); DROP TABLE t1;");
+
+ if( p->errCode==SQLITE_OK ){
+ sqlite3 *db = p->dbOut;
+ sqlite3_backup *pBackup = sqlite3_backup_init(db, "main", db2, "main");
+ if( pBackup ){
+ sqlite3_backup_step(pBackup, -1);
+ p->errCode = sqlite3_backup_finish(pBackup);
+ }else{
+ recoverDbError(p, db);
+ }
+ }
+
+ sqlite3_close(db2);
+ }
+}
+
+/*
+** This function is a no-op if recover handle p already contains an error
+** (if p->errCode!=SQLITE_OK). A copy of the error code is returned in
+** this case.
+**
+** Otherwise, an attempt is made to open the output database, attach
+** and create the schema of the temporary database used to store
+** intermediate data, and to register all required user functions and
+** virtual table modules with the output handle.
+**
+** If no error occurs, SQLITE_OK is returned. Otherwise, an error code
+** and error message are left in the recover handle and a copy of the
+** error code returned.
+*/
+static int recoverOpenOutput(sqlite3_recover *p){
+ struct Func {
+ const char *zName;
+ int nArg;
+ void (*xFunc)(sqlite3_context*,int,sqlite3_value **);
+ } aFunc[] = {
+ { "getpage", 1, recoverGetPage },
+ { "page_is_used", 1, recoverPageIsUsed },
+ { "read_i32", 2, recoverReadI32 },
+ { "escape_crnl", 1, recoverEscapeCrnl },
+ };
+
+ const int flags = SQLITE_OPEN_URI|SQLITE_OPEN_CREATE|SQLITE_OPEN_READWRITE;
+ sqlite3 *db = 0; /* New database handle */
+ int ii; /* For iterating through aFunc[] */
+
+ assert( p->dbOut==0 );
+
+ if( sqlite3_open_v2(p->zUri, &db, flags, 0) ){
+ recoverDbError(p, db);
+ }
+
+ /* Register the sqlite_dbdata and sqlite_dbptr virtual table modules.
+ ** These two are registered with the output database handle - this
+ ** module depends on the input handle supporting the sqlite_dbpage
+ ** virtual table only. */
+ if( p->errCode==SQLITE_OK ){
+ p->errCode = sqlite3_dbdata_init(db, 0, 0);
+ }
+
+ /* Register the custom user-functions with the output handle. */
+ for(ii=0; p->errCode==SQLITE_OK && ii<sizeof(aFunc)/sizeof(aFunc[0]); ii++){
+ p->errCode = sqlite3_create_function(db, aFunc[ii].zName,
+ aFunc[ii].nArg, SQLITE_UTF8, (void*)p, aFunc[ii].xFunc, 0, 0
+ );
+ }
+
+ p->dbOut = db;
+ return p->errCode;
+}
+
+/*
+** Attach the auxiliary database 'recovery' to the output database handle.
+** This temporary database is used during the recovery process and then
+** discarded.
+*/
+static void recoverOpenRecovery(sqlite3_recover *p){
+ char *zSql = recoverMPrintf(p, "ATTACH %Q AS recovery;", p->zStateDb);
+ recoverExec(p, p->dbOut, zSql);
+ recoverExec(p, p->dbOut,
+ "PRAGMA writable_schema = 1;"
+ "CREATE TABLE recovery.map(pgno INTEGER PRIMARY KEY, parent INT);"
+ "CREATE TABLE recovery.schema(type, name, tbl_name, rootpage, sql);"
+ );
+ sqlite3_free(zSql);
+}
+
+
+/*
+** This function is a no-op if recover handle p already contains an error
+** (if p->errCode!=SQLITE_OK).
+**
+** Otherwise, argument zName must be the name of a table that has just been
+** created in the output database. This function queries the output db
+** for the schema of said table, and creates a RecoverTable object to
+** store the schema in memory. The new RecoverTable object is linked into
+** the list at sqlite3_recover.pTblList.
+**
+** Parameter iRoot must be the root page of table zName in the INPUT
+** database.
+*/
+static void recoverAddTable(
+ sqlite3_recover *p,
+ const char *zName, /* Name of table created in output db */
+ i64 iRoot /* Root page of same table in INPUT db */
+){
+ sqlite3_stmt *pStmt = recoverPreparePrintf(p, p->dbOut,
+ "PRAGMA table_xinfo(%Q)", zName
+ );
+
+ if( pStmt ){
+ int iPk = -1;
+ int iBind = 1;
+ RecoverTable *pNew = 0;
+ int nCol = 0;
+ int nName = recoverStrlen(zName);
+ int nByte = 0;
+ while( sqlite3_step(pStmt)==SQLITE_ROW ){
+ nCol++;
+ nByte += (sqlite3_column_bytes(pStmt, 1)+1);
+ }
+ nByte += sizeof(RecoverTable) + nCol*sizeof(RecoverColumn) + nName+1;
+ recoverReset(p, pStmt);
+
+ pNew = recoverMalloc(p, nByte);
+ if( pNew ){
+ int i = 0;
+ int iField = 0;
+ char *csr = 0;
+ pNew->aCol = (RecoverColumn*)&pNew[1];
+ pNew->zTab = csr = (char*)&pNew->aCol[nCol];
+ pNew->nCol = nCol;
+ pNew->iRoot = iRoot;
+ memcpy(csr, zName, nName);
+ csr += nName+1;
+
+ for(i=0; sqlite3_step(pStmt)==SQLITE_ROW; i++){
+ int iPKF = sqlite3_column_int(pStmt, 5);
+ int n = sqlite3_column_bytes(pStmt, 1);
+ const char *z = (const char*)sqlite3_column_text(pStmt, 1);
+ const char *zType = (const char*)sqlite3_column_text(pStmt, 2);
+ int eHidden = sqlite3_column_int(pStmt, 6);
+
+ if( iPk==-1 && iPKF==1 && !sqlite3_stricmp("integer", zType) ) iPk = i;
+ if( iPKF>1 ) iPk = -2;
+ pNew->aCol[i].zCol = csr;
+ pNew->aCol[i].eHidden = eHidden;
+ if( eHidden==RECOVER_EHIDDEN_VIRTUAL ){
+ pNew->aCol[i].iField = -1;
+ }else{
+ pNew->aCol[i].iField = iField++;
+ }
+ if( eHidden!=RECOVER_EHIDDEN_VIRTUAL
+ && eHidden!=RECOVER_EHIDDEN_STORED
+ ){
+ pNew->aCol[i].iBind = iBind++;
+ }
+ memcpy(csr, z, n);
+ csr += (n+1);
+ }
+
+ pNew->pNext = p->pTblList;
+ p->pTblList = pNew;
+ pNew->bIntkey = 1;
+ }
+
+ recoverFinalize(p, pStmt);
+
+ pStmt = recoverPreparePrintf(p, p->dbOut, "PRAGMA index_xinfo(%Q)", zName);
+ while( pStmt && sqlite3_step(pStmt)==SQLITE_ROW ){
+ int iField = sqlite3_column_int(pStmt, 0);
+ int iCol = sqlite3_column_int(pStmt, 1);
+
+ assert( iField<pNew->nCol && iCol<pNew->nCol );
+ pNew->aCol[iCol].iField = iField;
+
+ pNew->bIntkey = 0;
+ iPk = -2;
+ }
+ recoverFinalize(p, pStmt);
+
+ if( p->errCode==SQLITE_OK ){
+ if( iPk>=0 ){
+ pNew->aCol[iPk].bIPK = 1;
+ }else if( pNew->bIntkey ){
+ pNew->iRowidBind = iBind++;
+ }
+ }
+ }
+}
+
+/*
+** This function is called after recoverCacheSchema() has cached those parts
+** of the input database schema that could be recovered in temporary table
+** "recovery.schema". This function creates in the output database copies
+** of all parts of that schema that must be created before the tables can
+** be populated. Specifically, this means:
+**
+** * all tables that are not VIRTUAL, and
+** * UNIQUE indexes.
+**
+** If the recovery handle uses SQL callbacks, then callbacks containing
+** the associated "CREATE TABLE" and "CREATE INDEX" statements are made.
+**
+** Additionally, records are added to the sqlite_schema table of the
+** output database for any VIRTUAL tables. The CREATE VIRTUAL TABLE
+** records are written directly to sqlite_schema, not actually executed.
+** If the handle is in SQL callback mode, then callbacks are invoked
+** with equivalent SQL statements.
+*/
+static int recoverWriteSchema1(sqlite3_recover *p){
+ sqlite3_stmt *pSelect = 0;
+ sqlite3_stmt *pTblname = 0;
+
+ pSelect = recoverPrepare(p, p->dbOut,
+ "WITH dbschema(rootpage, name, sql, tbl, isVirtual, isIndex) AS ("
+ " SELECT rootpage, name, sql, "
+ " type='table', "
+ " sql LIKE 'create virtual%',"
+ " (type='index' AND (sql LIKE '%unique%' OR ?1))"
+ " FROM recovery.schema"
+ ")"
+ "SELECT rootpage, tbl, isVirtual, name, sql"
+ " FROM dbschema "
+ " WHERE tbl OR isIndex"
+ " ORDER BY tbl DESC, name=='sqlite_sequence' DESC"
+ );
+
+ pTblname = recoverPrepare(p, p->dbOut,
+ "SELECT name FROM sqlite_schema "
+ "WHERE type='table' ORDER BY rowid DESC LIMIT 1"
+ );
+
+ if( pSelect ){
+ sqlite3_bind_int(pSelect, 1, p->bSlowIndexes);
+ while( sqlite3_step(pSelect)==SQLITE_ROW ){
+ i64 iRoot = sqlite3_column_int64(pSelect, 0);
+ int bTable = sqlite3_column_int(pSelect, 1);
+ int bVirtual = sqlite3_column_int(pSelect, 2);
+ const char *zName = (const char*)sqlite3_column_text(pSelect, 3);
+ const char *zSql = (const char*)sqlite3_column_text(pSelect, 4);
+ char *zFree = 0;
+ int rc = SQLITE_OK;
+
+ if( bVirtual ){
+ zSql = (const char*)(zFree = recoverMPrintf(p,
+ "INSERT INTO sqlite_schema VALUES('table', %Q, %Q, 0, %Q)",
+ zName, zName, zSql
+ ));
+ }
+ rc = sqlite3_exec(p->dbOut, zSql, 0, 0, 0);
+ if( rc==SQLITE_OK ){
+ recoverSqlCallback(p, zSql);
+ if( bTable && !bVirtual ){
+ if( SQLITE_ROW==sqlite3_step(pTblname) ){
+ const char *zTbl = (const char*)sqlite3_column_text(pTblname, 0);
+ recoverAddTable(p, zTbl, iRoot);
+ }
+ recoverReset(p, pTblname);
+ }
+ }else if( rc!=SQLITE_ERROR ){
+ recoverDbError(p, p->dbOut);
+ }
+ sqlite3_free(zFree);
+ }
+ }
+ recoverFinalize(p, pSelect);
+ recoverFinalize(p, pTblname);
+
+ return p->errCode;
+}
+
+/*
+** This function is called after the output database has been populated. It
+** adds all recovered schema elements that were not created in the output
+** database by recoverWriteSchema1() - everything except for tables and
+** UNIQUE indexes. Specifically:
+**
+** * views,
+** * triggers,
+** * non-UNIQUE indexes.
+**
+** If the recover handle is in SQL callback mode, then equivalent callbacks
+** are issued to create the schema elements.
+*/
+static int recoverWriteSchema2(sqlite3_recover *p){
+ sqlite3_stmt *pSelect = 0;
+
+ pSelect = recoverPrepare(p, p->dbOut,
+ p->bSlowIndexes ?
+ "SELECT rootpage, sql FROM recovery.schema "
+ " WHERE type!='table' AND type!='index'"
+ :
+ "SELECT rootpage, sql FROM recovery.schema "
+ " WHERE type!='table' AND (type!='index' OR sql NOT LIKE '%unique%')"
+ );
+
+ if( pSelect ){
+ while( sqlite3_step(pSelect)==SQLITE_ROW ){
+ const char *zSql = (const char*)sqlite3_column_text(pSelect, 1);
+ int rc = sqlite3_exec(p->dbOut, zSql, 0, 0, 0);
+ if( rc==SQLITE_OK ){
+ recoverSqlCallback(p, zSql);
+ }else if( rc!=SQLITE_ERROR ){
+ recoverDbError(p, p->dbOut);
+ }
+ }
+ }
+ recoverFinalize(p, pSelect);
+
+ return p->errCode;
+}
+
+/*
+** This function is a no-op if recover handle p already contains an error
+** (if p->errCode!=SQLITE_OK). In this case it returns NULL.
+**
+** Otherwise, if the recover handle is configured to create an output
+** database (was created by sqlite3_recover_init()), then this function
+** prepares and returns an SQL statement to INSERT a new record into table
+** pTab, assuming the first nField fields of a record extracted from disk
+** are valid.
+**
+** For example, if table pTab is:
+**
+** CREATE TABLE name(a, b GENERATED ALWAYS AS (a+1) STORED, c, d, e);
+**
+** And nField is 4, then the SQL statement prepared and returned is:
+**
+** INSERT INTO (a, c, d) VALUES (?1, ?2, ?3);
+**
+** In this case even though 4 values were extracted from the input db,
+** only 3 are written to the output, as the generated STORED column
+** cannot be written.
+**
+** If the recover handle is in SQL callback mode, then the SQL statement
+** prepared is such that evaluating it returns a single row containing
+** a single text value - itself an SQL statement similar to the above,
+** except with SQL literals in place of the variables. For example:
+**
+** SELECT 'INSERT INTO (a, c, d) VALUES ('
+** || quote(?1) || ', '
+** || quote(?2) || ', '
+** || quote(?3) || ')';
+**
+** In either case, it is the responsibility of the caller to eventually
+** free the statement handle using sqlite3_finalize().
+*/
+static sqlite3_stmt *recoverInsertStmt(
+ sqlite3_recover *p,
+ RecoverTable *pTab,
+ int nField
+){
+ sqlite3_stmt *pRet = 0;
+ const char *zSep = "";
+ const char *zSqlSep = "";
+ char *zSql = 0;
+ char *zFinal = 0;
+ char *zBind = 0;
+ int ii;
+ int bSql = p->xSql ? 1 : 0;
+
+ if( nField<=0 ) return 0;
+
+ assert( nField<=pTab->nCol );
+
+ zSql = recoverMPrintf(p, "INSERT OR IGNORE INTO %Q(", pTab->zTab);
+
+ if( pTab->iRowidBind ){
+ assert( pTab->bIntkey );
+ zSql = recoverMPrintf(p, "%z_rowid_", zSql);
+ if( bSql ){
+ zBind = recoverMPrintf(p, "%zquote(?%d)", zBind, pTab->iRowidBind);
+ }else{
+ zBind = recoverMPrintf(p, "%z?%d", zBind, pTab->iRowidBind);
+ }
+ zSqlSep = "||', '||";
+ zSep = ", ";
+ }
+
+ for(ii=0; ii<nField; ii++){
+ int eHidden = pTab->aCol[ii].eHidden;
+ if( eHidden!=RECOVER_EHIDDEN_VIRTUAL
+ && eHidden!=RECOVER_EHIDDEN_STORED
+ ){
+ assert( pTab->aCol[ii].iField>=0 && pTab->aCol[ii].iBind>=1 );
+ zSql = recoverMPrintf(p, "%z%s%Q", zSql, zSep, pTab->aCol[ii].zCol);
+
+ if( bSql ){
+ zBind = recoverMPrintf(p,
+ "%z%sescape_crnl(quote(?%d))", zBind, zSqlSep, pTab->aCol[ii].iBind
+ );
+ zSqlSep = "||', '||";
+ }else{
+ zBind = recoverMPrintf(p, "%z%s?%d", zBind, zSep, pTab->aCol[ii].iBind);
+ }
+ zSep = ", ";
+ }
+ }
+
+ if( bSql ){
+ zFinal = recoverMPrintf(p, "SELECT %Q || ') VALUES (' || %s || ')'",
+ zSql, zBind
+ );
+ }else{
+ zFinal = recoverMPrintf(p, "%s) VALUES (%s)", zSql, zBind);
+ }
+
+ pRet = recoverPrepare(p, p->dbOut, zFinal);
+ sqlite3_free(zSql);
+ sqlite3_free(zBind);
+ sqlite3_free(zFinal);
+
+ return pRet;
+}
+
+
+/*
+** Search the list of RecoverTable objects at p->pTblList for one that
+** has root page iRoot in the input database. If such an object is found,
+** return a pointer to it. Otherwise, return NULL.
+*/
+static RecoverTable *recoverFindTable(sqlite3_recover *p, u32 iRoot){
+ RecoverTable *pRet = 0;
+ for(pRet=p->pTblList; pRet && pRet->iRoot!=iRoot; pRet=pRet->pNext);
+ return pRet;
+}
+
+/*
+** This function attempts to create a lost and found table within the
+** output db. If successful, it returns a pointer to a buffer containing
+** the name of the new table. It is the responsibility of the caller to
+** eventually free this buffer using sqlite3_free().
+**
+** If an error occurs, NULL is returned and an error code and error
+** message left in the recover handle.
+*/
+static char *recoverLostAndFoundCreate(
+ sqlite3_recover *p, /* Recover object */
+ int nField /* Number of column fields in new table */
+){
+ char *zTbl = 0;
+ sqlite3_stmt *pProbe = 0;
+ int ii = 0;
+
+ pProbe = recoverPrepare(p, p->dbOut,
+ "SELECT 1 FROM sqlite_schema WHERE name=?"
+ );
+ for(ii=-1; zTbl==0 && p->errCode==SQLITE_OK && ii<1000; ii++){
+ int bFail = 0;
+ if( ii<0 ){
+ zTbl = recoverMPrintf(p, "%s", p->zLostAndFound);
+ }else{
+ zTbl = recoverMPrintf(p, "%s_%d", p->zLostAndFound, ii);
+ }
+
+ if( p->errCode==SQLITE_OK ){
+ sqlite3_bind_text(pProbe, 1, zTbl, -1, SQLITE_STATIC);
+ if( SQLITE_ROW==sqlite3_step(pProbe) ){
+ bFail = 1;
+ }
+ recoverReset(p, pProbe);
+ }
+
+ if( bFail ){
+ sqlite3_clear_bindings(pProbe);
+ sqlite3_free(zTbl);
+ zTbl = 0;
+ }
+ }
+ recoverFinalize(p, pProbe);
+
+ if( zTbl ){
+ const char *zSep = 0;
+ char *zField = 0;
+ char *zSql = 0;
+
+ zSep = "rootpgno INTEGER, pgno INTEGER, nfield INTEGER, id INTEGER, ";
+ for(ii=0; p->errCode==SQLITE_OK && ii<nField; ii++){
+ zField = recoverMPrintf(p, "%z%sc%d", zField, zSep, ii);
+ zSep = ", ";
+ }
+
+ zSql = recoverMPrintf(p, "CREATE TABLE %s(%s)", zTbl, zField);
+ sqlite3_free(zField);
+
+ recoverExec(p, p->dbOut, zSql);
+ recoverSqlCallback(p, zSql);
+ sqlite3_free(zSql);
+ }else if( p->errCode==SQLITE_OK ){
+ recoverError(
+ p, SQLITE_ERROR, "failed to create %s output table", p->zLostAndFound
+ );
+ }
+
+ return zTbl;
+}
+
+/*
+** Synthesize and prepare an INSERT statement to write to the lost_and_found
+** table in the output database. The name of the table is zTab, and it has
+** nField c* fields.
+*/
+static sqlite3_stmt *recoverLostAndFoundInsert(
+ sqlite3_recover *p,
+ const char *zTab,
+ int nField
+){
+ int nTotal = nField + 4;
+ int ii;
+ char *zBind = 0;
+ sqlite3_stmt *pRet = 0;
+
+ if( p->xSql==0 ){
+ for(ii=0; ii<nTotal; ii++){
+ zBind = recoverMPrintf(p, "%z%s?", zBind, zBind?", ":"", ii);
+ }
+ pRet = recoverPreparePrintf(
+ p, p->dbOut, "INSERT INTO %s VALUES(%s)", zTab, zBind
+ );
+ }else{
+ const char *zSep = "";
+ for(ii=0; ii<nTotal; ii++){
+ zBind = recoverMPrintf(p, "%z%squote(?)", zBind, zSep);
+ zSep = "|| ', ' ||";
+ }
+ pRet = recoverPreparePrintf(
+ p, p->dbOut, "SELECT 'INSERT INTO %s VALUES(' || %s || ')'", zTab, zBind
+ );
+ }
+
+ sqlite3_free(zBind);
+ return pRet;
+}
+
+/*
+** Input database page iPg contains data that will be written to the
+** lost-and-found table of the output database. This function attempts
+** to identify the root page of the tree that page iPg belonged to.
+** If successful, it sets output variable (*piRoot) to the page number
+** of the root page and returns SQLITE_OK. Otherwise, if an error occurs,
+** an SQLite error code is returned and the final value of *piRoot
+** undefined.
+*/
+static int recoverLostAndFoundFindRoot(
+ sqlite3_recover *p,
+ i64 iPg,
+ i64 *piRoot
+){
+ RecoverStateLAF *pLaf = &p->laf;
+
+ if( pLaf->pFindRoot==0 ){
+ pLaf->pFindRoot = recoverPrepare(p, p->dbOut,
+ "WITH RECURSIVE p(pgno) AS ("
+ " SELECT ?"
+ " UNION"
+ " SELECT parent FROM recovery.map AS m, p WHERE m.pgno=p.pgno"
+ ") "
+ "SELECT p.pgno FROM p, recovery.map m WHERE m.pgno=p.pgno "
+ " AND m.parent IS NULL"
+ );
+ }
+ if( p->errCode==SQLITE_OK ){
+ sqlite3_bind_int64(pLaf->pFindRoot, 1, iPg);
+ if( sqlite3_step(pLaf->pFindRoot)==SQLITE_ROW ){
+ *piRoot = sqlite3_column_int64(pLaf->pFindRoot, 0);
+ }else{
+ *piRoot = iPg;
+ }
+ recoverReset(p, pLaf->pFindRoot);
+ }
+ return p->errCode;
+}
+
+/*
+** Recover data from page iPage of the input database and write it to
+** the lost-and-found table in the output database.
+*/
+static void recoverLostAndFoundOnePage(sqlite3_recover *p, i64 iPage){
+ RecoverStateLAF *pLaf = &p->laf;
+ sqlite3_value **apVal = pLaf->apVal;
+ sqlite3_stmt *pPageData = pLaf->pPageData;
+ sqlite3_stmt *pInsert = pLaf->pInsert;
+
+ int nVal = -1;
+ int iPrevCell = 0;
+ i64 iRoot = 0;
+ int bHaveRowid = 0;
+ i64 iRowid = 0;
+ int ii = 0;
+
+ if( recoverLostAndFoundFindRoot(p, iPage, &iRoot) ) return;
+ sqlite3_bind_int64(pPageData, 1, iPage);
+ while( p->errCode==SQLITE_OK && SQLITE_ROW==sqlite3_step(pPageData) ){
+ int iCell = sqlite3_column_int64(pPageData, 0);
+ int iField = sqlite3_column_int64(pPageData, 1);
+
+ if( iPrevCell!=iCell && nVal>=0 ){
+ /* Insert the new row */
+ sqlite3_bind_int64(pInsert, 1, iRoot); /* rootpgno */
+ sqlite3_bind_int64(pInsert, 2, iPage); /* pgno */
+ sqlite3_bind_int(pInsert, 3, nVal); /* nfield */
+ if( bHaveRowid ){
+ sqlite3_bind_int64(pInsert, 4, iRowid); /* id */
+ }
+ for(ii=0; ii<nVal; ii++){
+ recoverBindValue(p, pInsert, 5+ii, apVal[ii]);
+ }
+ if( sqlite3_step(pInsert)==SQLITE_ROW ){
+ recoverSqlCallback(p, (const char*)sqlite3_column_text(pInsert, 0));
+ }
+ recoverReset(p, pInsert);
+
+ /* Discard the accumulated row data */
+ for(ii=0; ii<nVal; ii++){
+ sqlite3_value_free(apVal[ii]);
+ apVal[ii] = 0;
+ }
+ sqlite3_clear_bindings(pInsert);
+ bHaveRowid = 0;
+ nVal = -1;
+ }
+
+ if( iCell<0 ) break;
+
+ if( iField<0 ){
+ assert( nVal==-1 );
+ iRowid = sqlite3_column_int64(pPageData, 2);
+ bHaveRowid = 1;
+ nVal = 0;
+ }else if( iField<pLaf->nMaxField ){
+ sqlite3_value *pVal = sqlite3_column_value(pPageData, 2);
+ apVal[iField] = sqlite3_value_dup(pVal);
+ assert( iField==nVal || (nVal==-1 && iField==0) );
+ nVal = iField+1;
+ if( apVal[iField]==0 ){
+ recoverError(p, SQLITE_NOMEM, 0);
+ }
+ }
+
+ iPrevCell = iCell;
+ }
+ recoverReset(p, pPageData);
+
+ for(ii=0; ii<nVal; ii++){
+ sqlite3_value_free(apVal[ii]);
+ apVal[ii] = 0;
+ }
+}
+
+/*
+** Perform one step (sqlite3_recover_step()) of work for the connection
+** passed as the only argument, which is guaranteed to be in
+** RECOVER_STATE_LOSTANDFOUND3 state - during which the lost-and-found
+** table of the output database is populated with recovered data that can
+** not be assigned to any recovered schema object.
+*/
+static int recoverLostAndFound3Step(sqlite3_recover *p){
+ RecoverStateLAF *pLaf = &p->laf;
+ if( p->errCode==SQLITE_OK ){
+ if( pLaf->pInsert==0 ){
+ return SQLITE_DONE;
+ }else{
+ if( p->errCode==SQLITE_OK ){
+ int res = sqlite3_step(pLaf->pAllPage);
+ if( res==SQLITE_ROW ){
+ i64 iPage = sqlite3_column_int64(pLaf->pAllPage, 0);
+ if( recoverBitmapQuery(pLaf->pUsed, iPage)==0 ){
+ recoverLostAndFoundOnePage(p, iPage);
+ }
+ }else{
+ recoverReset(p, pLaf->pAllPage);
+ return SQLITE_DONE;
+ }
+ }
+ }
+ }
+ return SQLITE_OK;
+}
+
+/*
+** Initialize resources required in RECOVER_STATE_LOSTANDFOUND3
+** state - during which the lost-and-found table of the output database
+** is populated with recovered data that can not be assigned to any
+** recovered schema object.
+*/
+static void recoverLostAndFound3Init(sqlite3_recover *p){
+ RecoverStateLAF *pLaf = &p->laf;
+
+ if( pLaf->nMaxField>0 ){
+ char *zTab = 0; /* Name of lost_and_found table */
+
+ zTab = recoverLostAndFoundCreate(p, pLaf->nMaxField);
+ pLaf->pInsert = recoverLostAndFoundInsert(p, zTab, pLaf->nMaxField);
+ sqlite3_free(zTab);
+
+ pLaf->pAllPage = recoverPreparePrintf(p, p->dbOut,
+ "WITH RECURSIVE seq(ii) AS ("
+ " SELECT 1 UNION ALL SELECT ii+1 FROM seq WHERE ii<%lld"
+ ")"
+ "SELECT ii FROM seq" , p->laf.nPg
+ );
+ pLaf->pPageData = recoverPrepare(p, p->dbOut,
+ "SELECT cell, field, value "
+ "FROM sqlite_dbdata('getpage()') d WHERE d.pgno=? "
+ "UNION ALL "
+ "SELECT -1, -1, -1"
+ );
+
+ pLaf->apVal = (sqlite3_value**)recoverMalloc(p,
+ pLaf->nMaxField*sizeof(sqlite3_value*)
+ );
+ }
+}
+
+/*
+** Initialize resources required in RECOVER_STATE_WRITING state - during which
+** tables recovered from the schema of the input database are populated with
+** recovered data.
+*/
+static int recoverWriteDataInit(sqlite3_recover *p){
+ RecoverStateW1 *p1 = &p->w1;
+ RecoverTable *pTbl = 0;
+ int nByte = 0;
+
+ /* Figure out the maximum number of columns for any table in the schema */
+ assert( p1->nMax==0 );
+ for(pTbl=p->pTblList; pTbl; pTbl=pTbl->pNext){
+ if( pTbl->nCol>p1->nMax ) p1->nMax = pTbl->nCol;
+ }
+
+ /* Allocate an array of (sqlite3_value*) in which to accumulate the values
+ ** that will be written to the output database in a single row. */
+ nByte = sizeof(sqlite3_value*) * (p1->nMax+1);
+ p1->apVal = (sqlite3_value**)recoverMalloc(p, nByte);
+ if( p1->apVal==0 ) return p->errCode;
+
+ /* Prepare the SELECT to loop through schema tables (pTbls) and the SELECT
+ ** to loop through cells that appear to belong to a single table (pSel). */
+ p1->pTbls = recoverPrepare(p, p->dbOut,
+ "SELECT rootpage FROM recovery.schema "
+ " WHERE type='table' AND (sql NOT LIKE 'create virtual%')"
+ " ORDER BY (tbl_name='sqlite_sequence') ASC"
+ );
+ p1->pSel = recoverPrepare(p, p->dbOut,
+ "WITH RECURSIVE pages(page) AS ("
+ " SELECT ?1"
+ " UNION"
+ " SELECT child FROM sqlite_dbptr('getpage()'), pages "
+ " WHERE pgno=page"
+ ") "
+ "SELECT page, cell, field, value "
+ "FROM sqlite_dbdata('getpage()') d, pages p WHERE p.page=d.pgno "
+ "UNION ALL "
+ "SELECT 0, 0, 0, 0"
+ );
+
+ return p->errCode;
+}
+
+/*
+** Clean up resources allocated by recoverWriteDataInit() (stuff in
+** sqlite3_recover.w1).
+*/
+static void recoverWriteDataCleanup(sqlite3_recover *p){
+ RecoverStateW1 *p1 = &p->w1;
+ int ii;
+ for(ii=0; ii<p1->nVal; ii++){
+ sqlite3_value_free(p1->apVal[ii]);
+ }
+ sqlite3_free(p1->apVal);
+ recoverFinalize(p, p1->pInsert);
+ recoverFinalize(p, p1->pTbls);
+ recoverFinalize(p, p1->pSel);
+ memset(p1, 0, sizeof(*p1));
+}
+
+/*
+** Perform one step (sqlite3_recover_step()) of work for the connection
+** passed as the only argument, which is guaranteed to be in
+** RECOVER_STATE_WRITING state - during which tables recovered from the
+** schema of the input database are populated with recovered data.
+*/
+static int recoverWriteDataStep(sqlite3_recover *p){
+ RecoverStateW1 *p1 = &p->w1;
+ sqlite3_stmt *pSel = p1->pSel;
+ sqlite3_value **apVal = p1->apVal;
+
+ if( p->errCode==SQLITE_OK && p1->pTab==0 ){
+ if( sqlite3_step(p1->pTbls)==SQLITE_ROW ){
+ i64 iRoot = sqlite3_column_int64(p1->pTbls, 0);
+ p1->pTab = recoverFindTable(p, iRoot);
+
+ recoverFinalize(p, p1->pInsert);
+ p1->pInsert = 0;
+
+ /* If this table is unknown, return early. The caller will invoke this
+ ** function again and it will move on to the next table. */
+ if( p1->pTab==0 ) return p->errCode;
+
+ /* If this is the sqlite_sequence table, delete any rows added by
+ ** earlier INSERT statements on tables with AUTOINCREMENT primary
+ ** keys before recovering its contents. The p1->pTbls SELECT statement
+ ** is rigged to deliver "sqlite_sequence" last of all, so we don't
+ ** worry about it being modified after it is recovered. */
+ if( sqlite3_stricmp("sqlite_sequence", p1->pTab->zTab)==0 ){
+ recoverExec(p, p->dbOut, "DELETE FROM sqlite_sequence");
+ recoverSqlCallback(p, "DELETE FROM sqlite_sequence");
+ }
+
+ /* Bind the root page of this table within the original database to
+ ** SELECT statement p1->pSel. The SELECT statement will then iterate
+ ** through cells that look like they belong to table pTab. */
+ sqlite3_bind_int64(pSel, 1, iRoot);
+
+ p1->nVal = 0;
+ p1->bHaveRowid = 0;
+ p1->iPrevPage = -1;
+ p1->iPrevCell = -1;
+ }else{
+ return SQLITE_DONE;
+ }
+ }
+ assert( p->errCode!=SQLITE_OK || p1->pTab );
+
+ if( p->errCode==SQLITE_OK && sqlite3_step(pSel)==SQLITE_ROW ){
+ RecoverTable *pTab = p1->pTab;
+
+ i64 iPage = sqlite3_column_int64(pSel, 0);
+ int iCell = sqlite3_column_int(pSel, 1);
+ int iField = sqlite3_column_int(pSel, 2);
+ sqlite3_value *pVal = sqlite3_column_value(pSel, 3);
+ int bNewCell = (p1->iPrevPage!=iPage || p1->iPrevCell!=iCell);
+
+ assert( bNewCell==0 || (iField==-1 || iField==0) );
+ assert( bNewCell || iField==p1->nVal || p1->nVal==pTab->nCol );
+
+ if( bNewCell ){
+ int ii = 0;
+ if( p1->nVal>=0 ){
+ if( p1->pInsert==0 || p1->nVal!=p1->nInsert ){
+ recoverFinalize(p, p1->pInsert);
+ p1->pInsert = recoverInsertStmt(p, pTab, p1->nVal);
+ p1->nInsert = p1->nVal;
+ }
+ if( p1->nVal>0 ){
+ sqlite3_stmt *pInsert = p1->pInsert;
+ for(ii=0; ii<pTab->nCol; ii++){
+ RecoverColumn *pCol = &pTab->aCol[ii];
+ int iBind = pCol->iBind;
+ if( iBind>0 ){
+ if( pCol->bIPK ){
+ sqlite3_bind_int64(pInsert, iBind, p1->iRowid);
+ }else if( pCol->iField<p1->nVal ){
+ recoverBindValue(p, pInsert, iBind, apVal[pCol->iField]);
+ }
+ }
+ }
+ if( p->bRecoverRowid && pTab->iRowidBind>0 && p1->bHaveRowid ){
+ sqlite3_bind_int64(pInsert, pTab->iRowidBind, p1->iRowid);
+ }
+ if( SQLITE_ROW==sqlite3_step(pInsert) ){
+ const char *z = (const char*)sqlite3_column_text(pInsert, 0);
+ recoverSqlCallback(p, z);
+ }
+ recoverReset(p, pInsert);
+ assert( p->errCode || pInsert );
+ if( pInsert ) sqlite3_clear_bindings(pInsert);
+ }
+ }
+
+ for(ii=0; ii<p1->nVal; ii++){
+ sqlite3_value_free(apVal[ii]);
+ apVal[ii] = 0;
+ }
+ p1->nVal = -1;
+ p1->bHaveRowid = 0;
+ }
+
+ if( iPage!=0 ){
+ if( iField<0 ){
+ p1->iRowid = sqlite3_column_int64(pSel, 3);
+ assert( p1->nVal==-1 );
+ p1->nVal = 0;
+ p1->bHaveRowid = 1;
+ }else if( iField<pTab->nCol ){
+ assert( apVal[iField]==0 );
+ apVal[iField] = sqlite3_value_dup( pVal );
+ if( apVal[iField]==0 ){
+ recoverError(p, SQLITE_NOMEM, 0);
+ }
+ p1->nVal = iField+1;
+ }
+ p1->iPrevCell = iCell;
+ p1->iPrevPage = iPage;
+ }
+ }else{
+ recoverReset(p, pSel);
+ p1->pTab = 0;
+ }
+
+ return p->errCode;
+}
+
+/*
+** Initialize resources required by sqlite3_recover_step() in
+** RECOVER_STATE_LOSTANDFOUND1 state - during which the set of pages not
+** already allocated to a recovered schema element is determined.
+*/
+static void recoverLostAndFound1Init(sqlite3_recover *p){
+ RecoverStateLAF *pLaf = &p->laf;
+ sqlite3_stmt *pStmt = 0;
+
+ assert( p->laf.pUsed==0 );
+ pLaf->nPg = recoverPageCount(p);
+ pLaf->pUsed = recoverBitmapAlloc(p, pLaf->nPg);
+
+ /* Prepare a statement to iterate through all pages that are part of any tree
+ ** in the recoverable part of the input database schema to the bitmap. And,
+ ** if !p->bFreelistCorrupt, add all pages that appear to be part of the
+ ** freelist. */
+ pStmt = recoverPrepare(
+ p, p->dbOut,
+ "WITH trunk(pgno) AS ("
+ " SELECT read_i32(getpage(1), 8) AS x WHERE x>0"
+ " UNION"
+ " SELECT read_i32(getpage(trunk.pgno), 0) AS x FROM trunk WHERE x>0"
+ "),"
+ "trunkdata(pgno, data) AS ("
+ " SELECT pgno, getpage(pgno) FROM trunk"
+ "),"
+ "freelist(data, n, freepgno) AS ("
+ " SELECT data, min(16384, read_i32(data, 1)-1), pgno FROM trunkdata"
+ " UNION ALL"
+ " SELECT data, n-1, read_i32(data, 2+n) FROM freelist WHERE n>=0"
+ "),"
+ ""
+ "roots(r) AS ("
+ " SELECT 1 UNION ALL"
+ " SELECT rootpage FROM recovery.schema WHERE rootpage>0"
+ "),"
+ "used(page) AS ("
+ " SELECT r FROM roots"
+ " UNION"
+ " SELECT child FROM sqlite_dbptr('getpage()'), used "
+ " WHERE pgno=page"
+ ") "
+ "SELECT page FROM used"
+ " UNION ALL "
+ "SELECT freepgno FROM freelist WHERE NOT ?"
+ );
+ if( pStmt ) sqlite3_bind_int(pStmt, 1, p->bFreelistCorrupt);
+ pLaf->pUsedPages = pStmt;
+}
+
+/*
+** Perform one step (sqlite3_recover_step()) of work for the connection
+** passed as the only argument, which is guaranteed to be in
+** RECOVER_STATE_LOSTANDFOUND1 state - during which the set of pages not
+** already allocated to a recovered schema element is determined.
+*/
+static int recoverLostAndFound1Step(sqlite3_recover *p){
+ RecoverStateLAF *pLaf = &p->laf;
+ int rc = p->errCode;
+ if( rc==SQLITE_OK ){
+ rc = sqlite3_step(pLaf->pUsedPages);
+ if( rc==SQLITE_ROW ){
+ i64 iPg = sqlite3_column_int64(pLaf->pUsedPages, 0);
+ recoverBitmapSet(pLaf->pUsed, iPg);
+ rc = SQLITE_OK;
+ }else{
+ recoverFinalize(p, pLaf->pUsedPages);
+ pLaf->pUsedPages = 0;
+ }
+ }
+ return rc;
+}
+
+/*
+** Initialize resources required by RECOVER_STATE_LOSTANDFOUND2
+** state - during which the pages identified in RECOVER_STATE_LOSTANDFOUND1
+** are sorted into sets that likely belonged to the same database tree.
+*/
+static void recoverLostAndFound2Init(sqlite3_recover *p){
+ RecoverStateLAF *pLaf = &p->laf;
+
+ assert( p->laf.pAllAndParent==0 );
+ assert( p->laf.pMapInsert==0 );
+ assert( p->laf.pMaxField==0 );
+ assert( p->laf.nMaxField==0 );
+
+ pLaf->pMapInsert = recoverPrepare(p, p->dbOut,
+ "INSERT OR IGNORE INTO recovery.map(pgno, parent) VALUES(?, ?)"
+ );
+ pLaf->pAllAndParent = recoverPreparePrintf(p, p->dbOut,
+ "WITH RECURSIVE seq(ii) AS ("
+ " SELECT 1 UNION ALL SELECT ii+1 FROM seq WHERE ii<%lld"
+ ")"
+ "SELECT pgno, child FROM sqlite_dbptr('getpage()') "
+ " UNION ALL "
+ "SELECT NULL, ii FROM seq", p->laf.nPg
+ );
+ pLaf->pMaxField = recoverPreparePrintf(p, p->dbOut,
+ "SELECT max(field)+1 FROM sqlite_dbdata('getpage') WHERE pgno = ?"
+ );
+}
+
+/*
+** Perform one step (sqlite3_recover_step()) of work for the connection
+** passed as the only argument, which is guaranteed to be in
+** RECOVER_STATE_LOSTANDFOUND2 state - during which the pages identified
+** in RECOVER_STATE_LOSTANDFOUND1 are sorted into sets that likely belonged
+** to the same database tree.
+*/
+static int recoverLostAndFound2Step(sqlite3_recover *p){
+ RecoverStateLAF *pLaf = &p->laf;
+ if( p->errCode==SQLITE_OK ){
+ int res = sqlite3_step(pLaf->pAllAndParent);
+ if( res==SQLITE_ROW ){
+ i64 iChild = sqlite3_column_int(pLaf->pAllAndParent, 1);
+ if( recoverBitmapQuery(pLaf->pUsed, iChild)==0 ){
+ sqlite3_bind_int64(pLaf->pMapInsert, 1, iChild);
+ sqlite3_bind_value(pLaf->pMapInsert, 2,
+ sqlite3_column_value(pLaf->pAllAndParent, 0)
+ );
+ sqlite3_step(pLaf->pMapInsert);
+ recoverReset(p, pLaf->pMapInsert);
+ sqlite3_bind_int64(pLaf->pMaxField, 1, iChild);
+ if( SQLITE_ROW==sqlite3_step(pLaf->pMaxField) ){
+ int nMax = sqlite3_column_int(pLaf->pMaxField, 0);
+ if( nMax>pLaf->nMaxField ) pLaf->nMaxField = nMax;
+ }
+ recoverReset(p, pLaf->pMaxField);
+ }
+ }else{
+ recoverFinalize(p, pLaf->pAllAndParent);
+ pLaf->pAllAndParent =0;
+ return SQLITE_DONE;
+ }
+ }
+ return p->errCode;
+}
+
+/*
+** Free all resources allocated as part of sqlite3_recover_step() calls
+** in one of the RECOVER_STATE_LOSTANDFOUND[123] states.
+*/
+static void recoverLostAndFoundCleanup(sqlite3_recover *p){
+ recoverBitmapFree(p->laf.pUsed);
+ p->laf.pUsed = 0;
+ sqlite3_finalize(p->laf.pUsedPages);
+ sqlite3_finalize(p->laf.pAllAndParent);
+ sqlite3_finalize(p->laf.pMapInsert);
+ sqlite3_finalize(p->laf.pMaxField);
+ sqlite3_finalize(p->laf.pFindRoot);
+ sqlite3_finalize(p->laf.pInsert);
+ sqlite3_finalize(p->laf.pAllPage);
+ sqlite3_finalize(p->laf.pPageData);
+ p->laf.pUsedPages = 0;
+ p->laf.pAllAndParent = 0;
+ p->laf.pMapInsert = 0;
+ p->laf.pMaxField = 0;
+ p->laf.pFindRoot = 0;
+ p->laf.pInsert = 0;
+ p->laf.pAllPage = 0;
+ p->laf.pPageData = 0;
+ sqlite3_free(p->laf.apVal);
+ p->laf.apVal = 0;
+}
+
+/*
+** Free all resources allocated as part of sqlite3_recover_step() calls.
+*/
+static void recoverFinalCleanup(sqlite3_recover *p){
+ RecoverTable *pTab = 0;
+ RecoverTable *pNext = 0;
+
+ recoverWriteDataCleanup(p);
+ recoverLostAndFoundCleanup(p);
+
+ for(pTab=p->pTblList; pTab; pTab=pNext){
+ pNext = pTab->pNext;
+ sqlite3_free(pTab);
+ }
+ p->pTblList = 0;
+ sqlite3_finalize(p->pGetPage);
+ p->pGetPage = 0;
+ sqlite3_file_control(p->dbIn, p->zDb, SQLITE_FCNTL_RESET_CACHE, 0);
+
+ {
+#ifndef NDEBUG
+ int res =
+#endif
+ sqlite3_close(p->dbOut);
+ assert( res==SQLITE_OK );
+ }
+ p->dbOut = 0;
+}
+
+/*
+** Decode and return an unsigned 16-bit big-endian integer value from
+** buffer a[].
+*/
+static u32 recoverGetU16(const u8 *a){
+ return (((u32)a[0])<<8) + ((u32)a[1]);
+}
+
+/*
+** Decode and return an unsigned 32-bit big-endian integer value from
+** buffer a[].
+*/
+static u32 recoverGetU32(const u8 *a){
+ return (((u32)a[0])<<24) + (((u32)a[1])<<16) + (((u32)a[2])<<8) + ((u32)a[3]);
+}
+
+/*
+** Decode an SQLite varint from buffer a[]. Write the decoded value to (*pVal)
+** and return the number of bytes consumed.
+*/
+static int recoverGetVarint(const u8 *a, i64 *pVal){
+ sqlite3_uint64 u = 0;
+ int i;
+ for(i=0; i<8; i++){
+ u = (u<<7) + (a[i]&0x7f);
+ if( (a[i]&0x80)==0 ){ *pVal = (sqlite3_int64)u; return i+1; }
+ }
+ u = (u<<8) + (a[i]&0xff);
+ *pVal = (sqlite3_int64)u;
+ return 9;
+}
+
+/*
+** The second argument points to a buffer n bytes in size. If this buffer
+** or a prefix thereof appears to contain a well-formed SQLite b-tree page,
+** return the page-size in bytes. Otherwise, if the buffer does not
+** appear to contain a well-formed b-tree page, return 0.
+*/
+static int recoverIsValidPage(u8 *aTmp, const u8 *a, int n){
+ u8 *aUsed = aTmp;
+ int nFrag = 0;
+ int nActual = 0;
+ int iFree = 0;
+ int nCell = 0; /* Number of cells on page */
+ int iCellOff = 0; /* Offset of cell array in page */
+ int iContent = 0;
+ int eType = 0;
+ int ii = 0;
+
+ eType = (int)a[0];
+ if( eType!=0x02 && eType!=0x05 && eType!=0x0A && eType!=0x0D ) return 0;
+
+ iFree = (int)recoverGetU16(&a[1]);
+ nCell = (int)recoverGetU16(&a[3]);
+ iContent = (int)recoverGetU16(&a[5]);
+ if( iContent==0 ) iContent = 65536;
+ nFrag = (int)a[7];
+
+ if( iContent>n ) return 0;
+
+ memset(aUsed, 0, n);
+ memset(aUsed, 0xFF, iContent);
+
+ /* Follow the free-list. This is the same format for all b-tree pages. */
+ if( iFree && iFree<=iContent ) return 0;
+ while( iFree ){
+ int iNext = 0;
+ int nByte = 0;
+ if( iFree>(n-4) ) return 0;
+ iNext = recoverGetU16(&a[iFree]);
+ nByte = recoverGetU16(&a[iFree+2]);
+ if( iFree+nByte>n ) return 0;
+ if( iNext && iNext<iFree+nByte ) return 0;
+ memset(&aUsed[iFree], 0xFF, nByte);
+ iFree = iNext;
+ }
+
+ /* Run through the cells */
+ if( eType==0x02 || eType==0x05 ){
+ iCellOff = 12;
+ }else{
+ iCellOff = 8;
+ }
+ if( (iCellOff + 2*nCell)>iContent ) return 0;
+ for(ii=0; ii<nCell; ii++){
+ int iByte;
+ i64 nPayload = 0;
+ int nByte = 0;
+ int iOff = recoverGetU16(&a[iCellOff + 2*ii]);
+ if( iOff<iContent || iOff>n ){
+ return 0;
+ }
+ if( eType==0x05 || eType==0x02 ) nByte += 4;
+ nByte += recoverGetVarint(&a[iOff+nByte], &nPayload);
+ if( eType==0x0D ){
+ i64 dummy = 0;
+ nByte += recoverGetVarint(&a[iOff+nByte], &dummy);
+ }
+ if( eType!=0x05 ){
+ int X = (eType==0x0D) ? n-35 : (((n-12)*64/255)-23);
+ int M = ((n-12)*32/255)-23;
+ int K = M+((nPayload-M)%(n-4));
+
+ if( nPayload<X ){
+ nByte += nPayload;
+ }else if( K<=X ){
+ nByte += K+4;
+ }else{
+ nByte += M+4;
+ }
+ }
+
+ if( iOff+nByte>n ){
+ return 0;
+ }
+ for(iByte=iOff; iByte<(iOff+nByte); iByte++){
+ if( aUsed[iByte]!=0 ){
+ return 0;
+ }
+ aUsed[iByte] = 0xFF;
+ }
+ }
+
+ nActual = 0;
+ for(ii=0; ii<n; ii++){
+ if( aUsed[ii]==0 ) nActual++;
+ }
+ return (nActual==nFrag);
+}
+
+
+static int recoverVfsClose(sqlite3_file*);
+static int recoverVfsRead(sqlite3_file*, void*, int iAmt, sqlite3_int64 iOfst);
+static int recoverVfsWrite(sqlite3_file*, const void*, int, sqlite3_int64);
+static int recoverVfsTruncate(sqlite3_file*, sqlite3_int64 size);
+static int recoverVfsSync(sqlite3_file*, int flags);
+static int recoverVfsFileSize(sqlite3_file*, sqlite3_int64 *pSize);
+static int recoverVfsLock(sqlite3_file*, int);
+static int recoverVfsUnlock(sqlite3_file*, int);
+static int recoverVfsCheckReservedLock(sqlite3_file*, int *pResOut);
+static int recoverVfsFileControl(sqlite3_file*, int op, void *pArg);
+static int recoverVfsSectorSize(sqlite3_file*);
+static int recoverVfsDeviceCharacteristics(sqlite3_file*);
+static int recoverVfsShmMap(sqlite3_file*, int, int, int, void volatile**);
+static int recoverVfsShmLock(sqlite3_file*, int offset, int n, int flags);
+static void recoverVfsShmBarrier(sqlite3_file*);
+static int recoverVfsShmUnmap(sqlite3_file*, int deleteFlag);
+static int recoverVfsFetch(sqlite3_file*, sqlite3_int64, int, void**);
+static int recoverVfsUnfetch(sqlite3_file *pFd, sqlite3_int64 iOff, void *p);
+
+static sqlite3_io_methods recover_methods = {
+ 2, /* iVersion */
+ recoverVfsClose,
+ recoverVfsRead,
+ recoverVfsWrite,
+ recoverVfsTruncate,
+ recoverVfsSync,
+ recoverVfsFileSize,
+ recoverVfsLock,
+ recoverVfsUnlock,
+ recoverVfsCheckReservedLock,
+ recoverVfsFileControl,
+ recoverVfsSectorSize,
+ recoverVfsDeviceCharacteristics,
+ recoverVfsShmMap,
+ recoverVfsShmLock,
+ recoverVfsShmBarrier,
+ recoverVfsShmUnmap,
+ recoverVfsFetch,
+ recoverVfsUnfetch
+};
+
+static int recoverVfsClose(sqlite3_file *pFd){
+ assert( pFd->pMethods!=&recover_methods );
+ return pFd->pMethods->xClose(pFd);
+}
+
+/*
+** Write value v to buffer a[] as a 16-bit big-endian unsigned integer.
+*/
+static void recoverPutU16(u8 *a, u32 v){
+ a[0] = (v>>8) & 0x00FF;
+ a[1] = (v>>0) & 0x00FF;
+}
+
+/*
+** Write value v to buffer a[] as a 32-bit big-endian unsigned integer.
+*/
+static void recoverPutU32(u8 *a, u32 v){
+ a[0] = (v>>24) & 0x00FF;
+ a[1] = (v>>16) & 0x00FF;
+ a[2] = (v>>8) & 0x00FF;
+ a[3] = (v>>0) & 0x00FF;
+}
+
+/*
+** Detect the page-size of the database opened by file-handle pFd by
+** searching the first part of the file for a well-formed SQLite b-tree
+** page. If parameter nReserve is non-zero, then as well as searching for
+** a b-tree page with zero reserved bytes, this function searches for one
+** with nReserve reserved bytes at the end of it.
+**
+** If successful, set variable p->detected_pgsz to the detected page-size
+** in bytes and return SQLITE_OK. Or, if no error occurs but no valid page
+** can be found, return SQLITE_OK but leave p->detected_pgsz set to 0. Or,
+** if an error occurs (e.g. an IO or OOM error), then an SQLite error code
+** is returned. The final value of p->detected_pgsz is undefined in this
+** case.
+*/
+static int recoverVfsDetectPagesize(
+ sqlite3_recover *p, /* Recover handle */
+ sqlite3_file *pFd, /* File-handle open on input database */
+ u32 nReserve, /* Possible nReserve value */
+ i64 nSz /* Size of database file in bytes */
+){
+ int rc = SQLITE_OK;
+ const int nMin = 512;
+ const int nMax = 65536;
+ const int nMaxBlk = 4;
+ u32 pgsz = 0;
+ int iBlk = 0;
+ u8 *aPg = 0;
+ u8 *aTmp = 0;
+ int nBlk = 0;
+
+ aPg = (u8*)sqlite3_malloc(2*nMax);
+ if( aPg==0 ) return SQLITE_NOMEM;
+ aTmp = &aPg[nMax];
+
+ nBlk = (nSz+nMax-1)/nMax;
+ if( nBlk>nMaxBlk ) nBlk = nMaxBlk;
+
+ do {
+ for(iBlk=0; rc==SQLITE_OK && iBlk<nBlk; iBlk++){
+ int nByte = (nSz>=((iBlk+1)*nMax)) ? nMax : (nSz % nMax);
+ memset(aPg, 0, nMax);
+ rc = pFd->pMethods->xRead(pFd, aPg, nByte, iBlk*nMax);
+ if( rc==SQLITE_OK ){
+ int pgsz2;
+ for(pgsz2=(pgsz ? pgsz*2 : nMin); pgsz2<=nMax; pgsz2=pgsz2*2){
+ int iOff;
+ for(iOff=0; iOff<nMax; iOff+=pgsz2){
+ if( recoverIsValidPage(aTmp, &aPg[iOff], pgsz2-nReserve) ){
+ pgsz = pgsz2;
+ break;
+ }
+ }
+ }
+ }
+ }
+ if( pgsz>(u32)p->detected_pgsz ){
+ p->detected_pgsz = pgsz;
+ p->nReserve = nReserve;
+ }
+ if( nReserve==0 ) break;
+ nReserve = 0;
+ }while( 1 );
+
+ p->detected_pgsz = pgsz;
+ sqlite3_free(aPg);
+ return rc;
+}
+
+/*
+** The xRead() method of the wrapper VFS. This is used to intercept calls
+** to read page 1 of the input database.
+*/
+static int recoverVfsRead(sqlite3_file *pFd, void *aBuf, int nByte, i64 iOff){
+ int rc = SQLITE_OK;
+ if( pFd->pMethods==&recover_methods ){
+ pFd->pMethods = recover_g.pMethods;
+ rc = pFd->pMethods->xRead(pFd, aBuf, nByte, iOff);
+ if( nByte==16 ){
+ sqlite3_randomness(16, aBuf);
+ }else
+ if( rc==SQLITE_OK && iOff==0 && nByte>=108 ){
+ /* Ensure that the database has a valid header file. The only fields
+ ** that really matter to recovery are:
+ **
+ ** + Database page size (16-bits at offset 16)
+ ** + Size of db in pages (32-bits at offset 28)
+ ** + Database encoding (32-bits at offset 56)
+ **
+ ** Also preserved are:
+ **
+ ** + first freelist page (32-bits at offset 32)
+ ** + size of freelist (32-bits at offset 36)
+ ** + the wal-mode flags (16-bits at offset 18)
+ **
+ ** We also try to preserve the auto-vacuum, incr-value, user-version
+ ** and application-id fields - all 32 bit quantities at offsets
+ ** 52, 60, 64 and 68. All other fields are set to known good values.
+ **
+ ** Byte offset 105 should also contain the page-size as a 16-bit
+ ** integer.
+ */
+ const int aPreserve[] = {32, 36, 52, 60, 64, 68};
+ u8 aHdr[108] = {
+ 0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66,
+ 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x20, 0x33, 0x00,
+ 0xFF, 0xFF, 0x01, 0x01, 0x00, 0x40, 0x20, 0x20,
+ 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04,
+ 0x00, 0x00, 0x10, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x2e, 0x5b, 0x30,
+
+ 0x0D, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00
+ };
+ u8 *a = (u8*)aBuf;
+
+ u32 pgsz = recoverGetU16(&a[16]);
+ u32 nReserve = a[20];
+ u32 enc = recoverGetU32(&a[56]);
+ u32 dbsz = 0;
+ i64 dbFileSize = 0;
+ int ii;
+ sqlite3_recover *p = recover_g.p;
+
+ if( pgsz==0x01 ) pgsz = 65536;
+ rc = pFd->pMethods->xFileSize(pFd, &dbFileSize);
+
+ if( rc==SQLITE_OK && p->detected_pgsz==0 ){
+ rc = recoverVfsDetectPagesize(p, pFd, nReserve, dbFileSize);
+ }
+ if( p->detected_pgsz ){
+ pgsz = p->detected_pgsz;
+ nReserve = p->nReserve;
+ }
+
+ if( pgsz ){
+ dbsz = dbFileSize / pgsz;
+ }
+ if( enc!=SQLITE_UTF8 && enc!=SQLITE_UTF16BE && enc!=SQLITE_UTF16LE ){
+ enc = SQLITE_UTF8;
+ }
+
+ sqlite3_free(p->pPage1Cache);
+ p->pPage1Cache = 0;
+ p->pPage1Disk = 0;
+
+ p->pgsz = nByte;
+ p->pPage1Cache = (u8*)recoverMalloc(p, nByte*2);
+ if( p->pPage1Cache ){
+ p->pPage1Disk = &p->pPage1Cache[nByte];
+ memcpy(p->pPage1Disk, aBuf, nByte);
+ aHdr[18] = a[18];
+ aHdr[19] = a[19];
+ recoverPutU32(&aHdr[28], dbsz);
+ recoverPutU32(&aHdr[56], enc);
+ recoverPutU16(&aHdr[105], pgsz-nReserve);
+ if( pgsz==65536 ) pgsz = 1;
+ recoverPutU16(&aHdr[16], pgsz);
+ aHdr[20] = nReserve;
+ for(ii=0; ii<sizeof(aPreserve)/sizeof(aPreserve[0]); ii++){
+ memcpy(&aHdr[aPreserve[ii]], &a[aPreserve[ii]], 4);
+ }
+ memcpy(aBuf, aHdr, sizeof(aHdr));
+ memset(&((u8*)aBuf)[sizeof(aHdr)], 0, nByte-sizeof(aHdr));
+
+ memcpy(p->pPage1Cache, aBuf, nByte);
+ }else{
+ rc = p->errCode;
+ }
+
+ }
+ pFd->pMethods = &recover_methods;
+ }else{
+ rc = pFd->pMethods->xRead(pFd, aBuf, nByte, iOff);
+ }
+ return rc;
+}
+
+/*
+** Used to make sqlite3_io_methods wrapper methods less verbose.
+*/
+#define RECOVER_VFS_WRAPPER(code) \
+ int rc = SQLITE_OK; \
+ if( pFd->pMethods==&recover_methods ){ \
+ pFd->pMethods = recover_g.pMethods; \
+ rc = code; \
+ pFd->pMethods = &recover_methods; \
+ }else{ \
+ rc = code; \
+ } \
+ return rc;
+
+/*
+** Methods of the wrapper VFS. All methods except for xRead() and xClose()
+** simply uninstall the sqlite3_io_methods wrapper, invoke the equivalent
+** method on the lower level VFS, then reinstall the wrapper before returning.
+** Those that return an integer value use the RECOVER_VFS_WRAPPER macro.
+*/
+static int recoverVfsWrite(
+ sqlite3_file *pFd, const void *aBuf, int nByte, i64 iOff
+){
+ RECOVER_VFS_WRAPPER (
+ pFd->pMethods->xWrite(pFd, aBuf, nByte, iOff)
+ );
+}
+static int recoverVfsTruncate(sqlite3_file *pFd, sqlite3_int64 size){
+ RECOVER_VFS_WRAPPER (
+ pFd->pMethods->xTruncate(pFd, size)
+ );
+}
+static int recoverVfsSync(sqlite3_file *pFd, int flags){
+ RECOVER_VFS_WRAPPER (
+ pFd->pMethods->xSync(pFd, flags)
+ );
+}
+static int recoverVfsFileSize(sqlite3_file *pFd, sqlite3_int64 *pSize){
+ RECOVER_VFS_WRAPPER (
+ pFd->pMethods->xFileSize(pFd, pSize)
+ );
+}
+static int recoverVfsLock(sqlite3_file *pFd, int eLock){
+ RECOVER_VFS_WRAPPER (
+ pFd->pMethods->xLock(pFd, eLock)
+ );
+}
+static int recoverVfsUnlock(sqlite3_file *pFd, int eLock){
+ RECOVER_VFS_WRAPPER (
+ pFd->pMethods->xUnlock(pFd, eLock)
+ );
+}
+static int recoverVfsCheckReservedLock(sqlite3_file *pFd, int *pResOut){
+ RECOVER_VFS_WRAPPER (
+ pFd->pMethods->xCheckReservedLock(pFd, pResOut)
+ );
+}
+static int recoverVfsFileControl(sqlite3_file *pFd, int op, void *pArg){
+ RECOVER_VFS_WRAPPER (
+ (pFd->pMethods ? pFd->pMethods->xFileControl(pFd, op, pArg) : SQLITE_NOTFOUND)
+ );
+}
+static int recoverVfsSectorSize(sqlite3_file *pFd){
+ RECOVER_VFS_WRAPPER (
+ pFd->pMethods->xSectorSize(pFd)
+ );
+}
+static int recoverVfsDeviceCharacteristics(sqlite3_file *pFd){
+ RECOVER_VFS_WRAPPER (
+ pFd->pMethods->xDeviceCharacteristics(pFd)
+ );
+}
+static int recoverVfsShmMap(
+ sqlite3_file *pFd, int iPg, int pgsz, int bExtend, void volatile **pp
+){
+ RECOVER_VFS_WRAPPER (
+ pFd->pMethods->xShmMap(pFd, iPg, pgsz, bExtend, pp)
+ );
+}
+static int recoverVfsShmLock(sqlite3_file *pFd, int offset, int n, int flags){
+ RECOVER_VFS_WRAPPER (
+ pFd->pMethods->xShmLock(pFd, offset, n, flags)
+ );
+}
+static void recoverVfsShmBarrier(sqlite3_file *pFd){
+ if( pFd->pMethods==&recover_methods ){
+ pFd->pMethods = recover_g.pMethods;
+ pFd->pMethods->xShmBarrier(pFd);
+ pFd->pMethods = &recover_methods;
+ }else{
+ pFd->pMethods->xShmBarrier(pFd);
+ }
+}
+static int recoverVfsShmUnmap(sqlite3_file *pFd, int deleteFlag){
+ RECOVER_VFS_WRAPPER (
+ pFd->pMethods->xShmUnmap(pFd, deleteFlag)
+ );
+}
+
+static int recoverVfsFetch(
+ sqlite3_file *pFd,
+ sqlite3_int64 iOff,
+ int iAmt,
+ void **pp
+){
+ *pp = 0;
+ return SQLITE_OK;
+}
+static int recoverVfsUnfetch(sqlite3_file *pFd, sqlite3_int64 iOff, void *p){
+ return SQLITE_OK;
+}
+
+/*
+** Install the VFS wrapper around the file-descriptor open on the input
+** database for recover handle p. Mutex RECOVER_MUTEX_ID must be held
+** when this function is called.
+*/
+static void recoverInstallWrapper(sqlite3_recover *p){
+ sqlite3_file *pFd = 0;
+ assert( recover_g.pMethods==0 );
+ recoverAssertMutexHeld();
+ sqlite3_file_control(p->dbIn, p->zDb, SQLITE_FCNTL_FILE_POINTER, (void*)&pFd);
+ assert( pFd==0 || pFd->pMethods!=&recover_methods );
+ if( pFd && pFd->pMethods ){
+ int iVersion = 1 + (pFd->pMethods->iVersion>1 && pFd->pMethods->xShmMap!=0);
+ recover_g.pMethods = pFd->pMethods;
+ recover_g.p = p;
+ recover_methods.iVersion = iVersion;
+ pFd->pMethods = &recover_methods;
+ }
+}
+
+/*
+** Uninstall the VFS wrapper that was installed around the file-descriptor open
+** on the input database for recover handle p. Mutex RECOVER_MUTEX_ID must be
+** held when this function is called.
+*/
+static void recoverUninstallWrapper(sqlite3_recover *p){
+ sqlite3_file *pFd = 0;
+ recoverAssertMutexHeld();
+ sqlite3_file_control(p->dbIn, p->zDb,SQLITE_FCNTL_FILE_POINTER,(void*)&pFd);
+ if( pFd && pFd->pMethods ){
+ pFd->pMethods = recover_g.pMethods;
+ recover_g.pMethods = 0;
+ recover_g.p = 0;
+ }
+}
+
+/*
+** This function does the work of a single sqlite3_recover_step() call. It
+** is guaranteed that the handle is not in an error state when this
+** function is called.
+*/
+static void recoverStep(sqlite3_recover *p){
+ assert( p && p->errCode==SQLITE_OK );
+ switch( p->eState ){
+ case RECOVER_STATE_INIT:
+ /* This is the very first call to sqlite3_recover_step() on this object.
+ */
+ recoverSqlCallback(p, "BEGIN");
+ recoverSqlCallback(p, "PRAGMA writable_schema = on");
+
+ recoverEnterMutex();
+ recoverInstallWrapper(p);
+
+ /* Open the output database. And register required virtual tables and
+ ** user functions with the new handle. */
+ recoverOpenOutput(p);
+
+ /* Open transactions on both the input and output databases. */
+ sqlite3_file_control(p->dbIn, p->zDb, SQLITE_FCNTL_RESET_CACHE, 0);
+ recoverExec(p, p->dbIn, "PRAGMA writable_schema = on");
+ recoverExec(p, p->dbIn, "BEGIN");
+ if( p->errCode==SQLITE_OK ) p->bCloseTransaction = 1;
+ recoverExec(p, p->dbIn, "SELECT 1 FROM sqlite_schema");
+ recoverTransferSettings(p);
+ recoverOpenRecovery(p);
+ recoverCacheSchema(p);
+
+ recoverUninstallWrapper(p);
+ recoverLeaveMutex();
+
+ recoverExec(p, p->dbOut, "BEGIN");
+
+ recoverWriteSchema1(p);
+ p->eState = RECOVER_STATE_WRITING;
+ break;
+
+ case RECOVER_STATE_WRITING: {
+ if( p->w1.pTbls==0 ){
+ recoverWriteDataInit(p);
+ }
+ if( SQLITE_DONE==recoverWriteDataStep(p) ){
+ recoverWriteDataCleanup(p);
+ if( p->zLostAndFound ){
+ p->eState = RECOVER_STATE_LOSTANDFOUND1;
+ }else{
+ p->eState = RECOVER_STATE_SCHEMA2;
+ }
+ }
+ break;
+ }
+
+ case RECOVER_STATE_LOSTANDFOUND1: {
+ if( p->laf.pUsed==0 ){
+ recoverLostAndFound1Init(p);
+ }
+ if( SQLITE_DONE==recoverLostAndFound1Step(p) ){
+ p->eState = RECOVER_STATE_LOSTANDFOUND2;
+ }
+ break;
+ }
+ case RECOVER_STATE_LOSTANDFOUND2: {
+ if( p->laf.pAllAndParent==0 ){
+ recoverLostAndFound2Init(p);
+ }
+ if( SQLITE_DONE==recoverLostAndFound2Step(p) ){
+ p->eState = RECOVER_STATE_LOSTANDFOUND3;
+ }
+ break;
+ }
+
+ case RECOVER_STATE_LOSTANDFOUND3: {
+ if( p->laf.pInsert==0 ){
+ recoverLostAndFound3Init(p);
+ }
+ if( SQLITE_DONE==recoverLostAndFound3Step(p) ){
+ p->eState = RECOVER_STATE_SCHEMA2;
+ }
+ break;
+ }
+
+ case RECOVER_STATE_SCHEMA2: {
+ int rc = SQLITE_OK;
+
+ recoverWriteSchema2(p);
+ p->eState = RECOVER_STATE_DONE;
+
+ /* If no error has occurred, commit the write transaction on the output
+ ** database. Regardless of whether or not an error has occurred, make
+ ** an attempt to end the read transaction on the input database. */
+ recoverExec(p, p->dbOut, "COMMIT");
+ rc = sqlite3_exec(p->dbIn, "END", 0, 0, 0);
+ if( p->errCode==SQLITE_OK ) p->errCode = rc;
+
+ recoverSqlCallback(p, "PRAGMA writable_schema = off");
+ recoverSqlCallback(p, "COMMIT");
+ p->eState = RECOVER_STATE_DONE;
+ recoverFinalCleanup(p);
+ break;
+ };
+
+ case RECOVER_STATE_DONE: {
+ /* no-op */
+ break;
+ };
+ }
+}
+
+
+/*
+** This is a worker function that does the heavy lifting for both init
+** functions:
+**
+** sqlite3_recover_init()
+** sqlite3_recover_init_sql()
+**
+** All this function does is allocate space for the recover handle and
+** take copies of the input parameters. All the real work is done within
+** sqlite3_recover_run().
+*/
+sqlite3_recover *recoverInit(
+ sqlite3* db,
+ const char *zDb,
+ const char *zUri, /* Output URI for _recover_init() */
+ int (*xSql)(void*, const char*),/* SQL callback for _recover_init_sql() */
+ void *pSqlCtx /* Context arg for _recover_init_sql() */
+){
+ sqlite3_recover *pRet = 0;
+ int nDb = 0;
+ int nUri = 0;
+ int nByte = 0;
+
+ if( zDb==0 ){ zDb = "main"; }
+
+ nDb = recoverStrlen(zDb);
+ nUri = recoverStrlen(zUri);
+
+ nByte = sizeof(sqlite3_recover) + nDb+1 + nUri+1;
+ pRet = (sqlite3_recover*)sqlite3_malloc(nByte);
+ if( pRet ){
+ memset(pRet, 0, nByte);
+ pRet->dbIn = db;
+ pRet->zDb = (char*)&pRet[1];
+ pRet->zUri = &pRet->zDb[nDb+1];
+ memcpy(pRet->zDb, zDb, nDb);
+ if( nUri>0 && zUri ) memcpy(pRet->zUri, zUri, nUri);
+ pRet->xSql = xSql;
+ pRet->pSqlCtx = pSqlCtx;
+ pRet->bRecoverRowid = RECOVER_ROWID_DEFAULT;
+ }
+
+ return pRet;
+}
+
+/*
+** Initialize a recovery handle that creates a new database containing
+** the recovered data.
+*/
+sqlite3_recover *sqlite3_recover_init(
+ sqlite3* db,
+ const char *zDb,
+ const char *zUri
+){
+ return recoverInit(db, zDb, zUri, 0, 0);
+}
+
+/*
+** Initialize a recovery handle that returns recovered data in the
+** form of SQL statements via a callback.
+*/
+sqlite3_recover *sqlite3_recover_init_sql(
+ sqlite3* db,
+ const char *zDb,
+ int (*xSql)(void*, const char*),
+ void *pSqlCtx
+){
+ return recoverInit(db, zDb, 0, xSql, pSqlCtx);
+}
+
+/*
+** Return the handle error message, if any.
+*/
+const char *sqlite3_recover_errmsg(sqlite3_recover *p){
+ return (p && p->errCode!=SQLITE_NOMEM) ? p->zErrMsg : "out of memory";
+}
+
+/*
+** Return the handle error code.
+*/
+int sqlite3_recover_errcode(sqlite3_recover *p){
+ return p ? p->errCode : SQLITE_NOMEM;
+}
+
+/*
+** Configure the handle.
+*/
+int sqlite3_recover_config(sqlite3_recover *p, int op, void *pArg){
+ int rc = SQLITE_OK;
+ if( p==0 ){
+ rc = SQLITE_NOMEM;
+ }else if( p->eState!=RECOVER_STATE_INIT ){
+ rc = SQLITE_MISUSE;
+ }else{
+ switch( op ){
+ case 789:
+ /* This undocumented magic configuration option is used to set the
+ ** name of the auxiliary database that is ATTACH-ed to the database
+ ** connection and used to hold state information during the
+ ** recovery process. This option is for debugging use only and
+ ** is subject to change or removal at any time. */
+ sqlite3_free(p->zStateDb);
+ p->zStateDb = recoverMPrintf(p, "%s", (char*)pArg);
+ break;
+
+ case SQLITE_RECOVER_LOST_AND_FOUND: {
+ const char *zArg = (const char*)pArg;
+ sqlite3_free(p->zLostAndFound);
+ if( zArg ){
+ p->zLostAndFound = recoverMPrintf(p, "%s", zArg);
+ }else{
+ p->zLostAndFound = 0;
+ }
+ break;
+ }
+
+ case SQLITE_RECOVER_FREELIST_CORRUPT:
+ p->bFreelistCorrupt = *(int*)pArg;
+ break;
+
+ case SQLITE_RECOVER_ROWIDS:
+ p->bRecoverRowid = *(int*)pArg;
+ break;
+
+ case SQLITE_RECOVER_SLOWINDEXES:
+ p->bSlowIndexes = *(int*)pArg;
+ break;
+
+ default:
+ rc = SQLITE_NOTFOUND;
+ break;
+ }
+ }
+
+ return rc;
+}
+
+/*
+** Do a unit of work towards the recovery job. Return SQLITE_OK if
+** no error has occurred but database recovery is not finished, SQLITE_DONE
+** if database recovery has been successfully completed, or an SQLite
+** error code if an error has occurred.
+*/
+int sqlite3_recover_step(sqlite3_recover *p){
+ if( p==0 ) return SQLITE_NOMEM;
+ if( p->errCode==SQLITE_OK ) recoverStep(p);
+ if( p->eState==RECOVER_STATE_DONE && p->errCode==SQLITE_OK ){
+ return SQLITE_DONE;
+ }
+ return p->errCode;
+}
+
+/*
+** Do the configured recovery operation. Return SQLITE_OK if successful, or
+** else an SQLite error code.
+*/
+int sqlite3_recover_run(sqlite3_recover *p){
+ while( SQLITE_OK==sqlite3_recover_step(p) );
+ return sqlite3_recover_errcode(p);
+}
+
+
+/*
+** Free all resources associated with the recover handle passed as the only
+** argument. The results of using a handle with any sqlite3_recover_**
+** API function after it has been passed to this function are undefined.
+**
+** A copy of the value returned by the first call made to sqlite3_recover_run()
+** on this handle is returned, or SQLITE_OK if sqlite3_recover_run() has
+** not been called on this handle.
+*/
+int sqlite3_recover_finish(sqlite3_recover *p){
+ int rc;
+ if( p==0 ){
+ rc = SQLITE_NOMEM;
+ }else{
+ recoverFinalCleanup(p);
+ if( p->bCloseTransaction && sqlite3_get_autocommit(p->dbIn)==0 ){
+ rc = sqlite3_exec(p->dbIn, "END", 0, 0, 0);
+ if( p->errCode==SQLITE_OK ) p->errCode = rc;
+ }
+ rc = p->errCode;
+ sqlite3_free(p->zErrMsg);
+ sqlite3_free(p->zStateDb);
+ sqlite3_free(p->zLostAndFound);
+ sqlite3_free(p->pPage1Cache);
+ sqlite3_free(p);
+ }
+ return rc;
+}
+
+#endif /* ifndef SQLITE_OMIT_VIRTUALTABLE */
diff --git a/ext/recover/sqlite3recover.h b/ext/recover/sqlite3recover.h
new file mode 100644
index 0000000..7a1cd1c
--- /dev/null
+++ b/ext/recover/sqlite3recover.h
@@ -0,0 +1,249 @@
+/*
+** 2022-08-27
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+**
+** This file contains the public interface to the "recover" extension -
+** an SQLite extension designed to recover data from corrupted database
+** files.
+*/
+
+/*
+** OVERVIEW:
+**
+** To use the API to recover data from a corrupted database, an
+** application:
+**
+** 1) Creates an sqlite3_recover handle by calling either
+** sqlite3_recover_init() or sqlite3_recover_init_sql().
+**
+** 2) Configures the new handle using one or more calls to
+** sqlite3_recover_config().
+**
+** 3) Executes the recovery by repeatedly calling sqlite3_recover_step() on
+** the handle until it returns something other than SQLITE_OK. If it
+** returns SQLITE_DONE, then the recovery operation completed without
+** error. If it returns some other non-SQLITE_OK value, then an error
+** has occurred.
+**
+** 4) Retrieves any error code and English language error message using the
+** sqlite3_recover_errcode() and sqlite3_recover_errmsg() APIs,
+** respectively.
+**
+** 5) Destroys the sqlite3_recover handle and frees all resources
+** using sqlite3_recover_finish().
+**
+** The application may abandon the recovery operation at any point
+** before it is finished by passing the sqlite3_recover handle to
+** sqlite3_recover_finish(). This is not an error, but the final state
+** of the output database, or the results of running the partial script
+** delivered to the SQL callback, are undefined.
+*/
+
+#ifndef _SQLITE_RECOVER_H
+#define _SQLITE_RECOVER_H
+
+#include "sqlite3.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+** An instance of the sqlite3_recover object represents a recovery
+** operation in progress.
+**
+** Constructors:
+**
+** sqlite3_recover_init()
+** sqlite3_recover_init_sql()
+**
+** Destructor:
+**
+** sqlite3_recover_finish()
+**
+** Methods:
+**
+** sqlite3_recover_config()
+** sqlite3_recover_errcode()
+** sqlite3_recover_errmsg()
+** sqlite3_recover_run()
+** sqlite3_recover_step()
+*/
+typedef struct sqlite3_recover sqlite3_recover;
+
+/*
+** These two APIs attempt to create and return a new sqlite3_recover object.
+** In both cases the first two arguments identify the (possibly
+** corrupt) database to recover data from. The first argument is an open
+** database handle and the second the name of a database attached to that
+** handle (i.e. "main", "temp" or the name of an attached database).
+**
+** If sqlite3_recover_init() is used to create the new sqlite3_recover
+** handle, then data is recovered into a new database, identified by
+** string parameter zUri. zUri may be an absolute or relative file path,
+** or may be an SQLite URI. If the identified database file already exists,
+** it is overwritten.
+**
+** If sqlite3_recover_init_sql() is invoked, then any recovered data will
+** be returned to the user as a series of SQL statements. Executing these
+** SQL statements results in the same database as would have been created
+** had sqlite3_recover_init() been used. For each SQL statement in the
+** output, the callback function passed as the third argument (xSql) is
+** invoked once. The first parameter is a passed a copy of the fourth argument
+** to this function (pCtx) as its first parameter, and a pointer to a
+** nul-terminated buffer containing the SQL statement formated as UTF-8 as
+** the second. If the xSql callback returns any value other than SQLITE_OK,
+** then processing is immediately abandoned and the value returned used as
+** the recover handle error code (see below).
+**
+** If an out-of-memory error occurs, NULL may be returned instead of
+** a valid handle. In all other cases, it is the responsibility of the
+** application to avoid resource leaks by ensuring that
+** sqlite3_recover_finish() is called on all allocated handles.
+*/
+sqlite3_recover *sqlite3_recover_init(
+ sqlite3* db,
+ const char *zDb,
+ const char *zUri
+);
+sqlite3_recover *sqlite3_recover_init_sql(
+ sqlite3* db,
+ const char *zDb,
+ int (*xSql)(void*, const char*),
+ void *pCtx
+);
+
+/*
+** Configure an sqlite3_recover object that has just been created using
+** sqlite3_recover_init() or sqlite3_recover_init_sql(). This function
+** may only be called before the first call to sqlite3_recover_step()
+** or sqlite3_recover_run() on the object.
+**
+** The second argument passed to this function must be one of the
+** SQLITE_RECOVER_* symbols defined below. Valid values for the third argument
+** depend on the specific SQLITE_RECOVER_* symbol in use.
+**
+** SQLITE_OK is returned if the configuration operation was successful,
+** or an SQLite error code otherwise.
+*/
+int sqlite3_recover_config(sqlite3_recover*, int op, void *pArg);
+
+/*
+** SQLITE_RECOVER_LOST_AND_FOUND:
+** The pArg argument points to a string buffer containing the name
+** of a "lost-and-found" table in the output database, or NULL. If
+** the argument is non-NULL and the database contains seemingly
+** valid pages that cannot be associated with any table in the
+** recovered part of the schema, data is extracted from these
+** pages to add to the lost-and-found table.
+**
+** SQLITE_RECOVER_FREELIST_CORRUPT:
+** The pArg value must actually be a pointer to a value of type
+** int containing value 0 or 1 cast as a (void*). If this option is set
+** (argument is 1) and a lost-and-found table has been configured using
+** SQLITE_RECOVER_LOST_AND_FOUND, then is assumed that the freelist is
+** corrupt and an attempt is made to recover records from pages that
+** appear to be linked into the freelist. Otherwise, pages on the freelist
+** are ignored. Setting this option can recover more data from the
+** database, but often ends up "recovering" deleted records. The default
+** value is 0 (clear).
+**
+** SQLITE_RECOVER_ROWIDS:
+** The pArg value must actually be a pointer to a value of type
+** int containing value 0 or 1 cast as a (void*). If this option is set
+** (argument is 1), then an attempt is made to recover rowid values
+** that are not also INTEGER PRIMARY KEY values. If this option is
+** clear, then new rowids are assigned to all recovered rows. The
+** default value is 1 (set).
+**
+** SQLITE_RECOVER_SLOWINDEXES:
+** The pArg value must actually be a pointer to a value of type
+** int containing value 0 or 1 cast as a (void*). If this option is clear
+** (argument is 0), then when creating an output database, the recover
+** module creates and populates non-UNIQUE indexes right at the end of the
+** recovery operation - after all recoverable data has been inserted
+** into the new database. This is faster overall, but means that the
+** final call to sqlite3_recover_step() for a recovery operation may
+** be need to create a large number of indexes, which may be very slow.
+**
+** Or, if this option is set (argument is 1), then non-UNIQUE indexes
+** are created in the output database before it is populated with
+** recovered data. This is slower overall, but avoids the slow call
+** to sqlite3_recover_step() at the end of the recovery operation.
+**
+** The default option value is 0.
+*/
+#define SQLITE_RECOVER_LOST_AND_FOUND 1
+#define SQLITE_RECOVER_FREELIST_CORRUPT 2
+#define SQLITE_RECOVER_ROWIDS 3
+#define SQLITE_RECOVER_SLOWINDEXES 4
+
+/*
+** Perform a unit of work towards the recovery operation. This function
+** must normally be called multiple times to complete database recovery.
+**
+** If no error occurs but the recovery operation is not completed, this
+** function returns SQLITE_OK. If recovery has been completed successfully
+** then SQLITE_DONE is returned. If an error has occurred, then an SQLite
+** error code (e.g. SQLITE_IOERR or SQLITE_NOMEM) is returned. It is not
+** considered an error if some or all of the data cannot be recovered
+** due to database corruption.
+**
+** Once sqlite3_recover_step() has returned a value other than SQLITE_OK,
+** all further such calls on the same recover handle are no-ops that return
+** the same non-SQLITE_OK value.
+*/
+int sqlite3_recover_step(sqlite3_recover*);
+
+/*
+** Run the recovery operation to completion. Return SQLITE_OK if successful,
+** or an SQLite error code otherwise. Calling this function is the same
+** as executing:
+**
+** while( SQLITE_OK==sqlite3_recover_step(p) );
+** return sqlite3_recover_errcode(p);
+*/
+int sqlite3_recover_run(sqlite3_recover*);
+
+/*
+** If an error has been encountered during a prior call to
+** sqlite3_recover_step(), then this function attempts to return a
+** pointer to a buffer containing an English language explanation of
+** the error. If no error message is available, or if an out-of memory
+** error occurs while attempting to allocate a buffer in which to format
+** the error message, NULL is returned.
+**
+** The returned buffer remains valid until the sqlite3_recover handle is
+** destroyed using sqlite3_recover_finish().
+*/
+const char *sqlite3_recover_errmsg(sqlite3_recover*);
+
+/*
+** If this function is called on an sqlite3_recover handle after
+** an error occurs, an SQLite error code is returned. Otherwise, SQLITE_OK.
+*/
+int sqlite3_recover_errcode(sqlite3_recover*);
+
+/*
+** Clean up a recovery object created by a call to sqlite3_recover_init().
+** The results of using a recovery object with any API after it has been
+** passed to this function are undefined.
+**
+** This function returns the same value as sqlite3_recover_errcode().
+*/
+int sqlite3_recover_finish(sqlite3_recover*);
+
+
+#ifdef __cplusplus
+} /* end of the 'extern "C"' block */
+#endif
+
+#endif /* ifndef _SQLITE_RECOVER_H */
diff --git a/ext/recover/test_recover.c b/ext/recover/test_recover.c
new file mode 100644
index 0000000..1c333df
--- /dev/null
+++ b/ext/recover/test_recover.c
@@ -0,0 +1,311 @@
+/*
+** 2022-08-27
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+**
+*/
+
+#include "sqlite3recover.h"
+#include "sqliteInt.h"
+
+#include <tcl.h>
+#include <assert.h>
+
+#ifndef SQLITE_OMIT_VIRTUALTABLE
+
+typedef struct TestRecover TestRecover;
+struct TestRecover {
+ sqlite3_recover *p;
+ Tcl_Interp *interp;
+ Tcl_Obj *pScript;
+};
+
+static int xSqlCallback(void *pSqlArg, const char *zSql){
+ TestRecover *p = (TestRecover*)pSqlArg;
+ Tcl_Obj *pEval = 0;
+ int res = 0;
+
+ pEval = Tcl_DuplicateObj(p->pScript);
+ Tcl_IncrRefCount(pEval);
+
+ res = Tcl_ListObjAppendElement(p->interp, pEval, Tcl_NewStringObj(zSql, -1));
+ if( res==TCL_OK ){
+ res = Tcl_EvalObjEx(p->interp, pEval, 0);
+ }
+
+ Tcl_DecrRefCount(pEval);
+ if( res ){
+ Tcl_BackgroundError(p->interp);
+ return TCL_ERROR;
+ }else{
+ Tcl_Obj *pObj = Tcl_GetObjResult(p->interp);
+ if( Tcl_GetCharLength(pObj)==0 ){
+ res = 0;
+ }else if( Tcl_GetIntFromObj(p->interp, pObj, &res) ){
+ Tcl_BackgroundError(p->interp);
+ return TCL_ERROR;
+ }
+ }
+ return res;
+}
+
+static int getDbPointer(Tcl_Interp *interp, Tcl_Obj *pObj, sqlite3 **pDb){
+ Tcl_CmdInfo info;
+ if( 0==Tcl_GetCommandInfo(interp, Tcl_GetString(pObj), &info) ){
+ Tcl_AppendResult(interp, "no such handle: ", Tcl_GetString(pObj), 0);
+ return TCL_ERROR;
+ }
+ *pDb = *(sqlite3 **)info.objClientData;
+ return TCL_OK;
+}
+
+/*
+** Implementation of the command created by [sqlite3_recover_init]:
+**
+** $cmd config OP ARG
+** $cmd run
+** $cmd errmsg
+** $cmd errcode
+** $cmd finalize
+*/
+static int testRecoverCmd(
+ void *clientData,
+ Tcl_Interp *interp,
+ int objc,
+ Tcl_Obj *CONST objv[]
+){
+ static struct RecoverSub {
+ const char *zSub;
+ int nArg;
+ const char *zMsg;
+ } aSub[] = {
+ { "config", 2, "ARG" }, /* 0 */
+ { "run", 0, "" }, /* 1 */
+ { "errmsg", 0, "" }, /* 2 */
+ { "errcode", 0, "" }, /* 3 */
+ { "finish", 0, "" }, /* 4 */
+ { "step", 0, "" }, /* 5 */
+ { 0 }
+ };
+ int rc = TCL_OK;
+ int iSub = 0;
+ TestRecover *pTest = (TestRecover*)clientData;
+
+ if( objc<2 ){
+ Tcl_WrongNumArgs(interp, 1, objv, "SUBCOMMAND ...");
+ return TCL_ERROR;
+ }
+ rc = Tcl_GetIndexFromObjStruct(interp,
+ objv[1], aSub, sizeof(aSub[0]), "sub-command", 0, &iSub
+ );
+ if( rc!=TCL_OK ) return rc;
+ if( (objc-2)!=aSub[iSub].nArg ){
+ Tcl_WrongNumArgs(interp, 2, objv, aSub[iSub].zMsg);
+ return TCL_ERROR;
+ }
+
+ switch( iSub ){
+ case 0: assert( sqlite3_stricmp("config", aSub[iSub].zSub)==0 ); {
+ const char *aOp[] = {
+ "testdb", /* 0 */
+ "lostandfound", /* 1 */
+ "freelistcorrupt", /* 2 */
+ "rowids", /* 3 */
+ "slowindexes", /* 4 */
+ "invalid", /* 5 */
+ 0
+ };
+ int iOp = 0;
+ int res = 0;
+ if( Tcl_GetIndexFromObj(interp, objv[2], aOp, "option", 0, &iOp) ){
+ return TCL_ERROR;
+ }
+ switch( iOp ){
+ case 0:
+ res = sqlite3_recover_config(pTest->p,
+ 789, (void*)Tcl_GetString(objv[3]) /* MAGIC NUMBER! */
+ );
+ break;
+ case 1: {
+ const char *zStr = Tcl_GetString(objv[3]);
+ res = sqlite3_recover_config(pTest->p,
+ SQLITE_RECOVER_LOST_AND_FOUND, (void*)(zStr[0] ? zStr : 0)
+ );
+ break;
+ }
+ case 2: {
+ int iVal = 0;
+ if( Tcl_GetBooleanFromObj(interp, objv[3], &iVal) ) return TCL_ERROR;
+ res = sqlite3_recover_config(pTest->p,
+ SQLITE_RECOVER_FREELIST_CORRUPT, (void*)&iVal
+ );
+ break;
+ }
+ case 3: {
+ int iVal = 0;
+ if( Tcl_GetBooleanFromObj(interp, objv[3], &iVal) ) return TCL_ERROR;
+ res = sqlite3_recover_config(pTest->p,
+ SQLITE_RECOVER_ROWIDS, (void*)&iVal
+ );
+ break;
+ }
+ case 4: {
+ int iVal = 0;
+ if( Tcl_GetBooleanFromObj(interp, objv[3], &iVal) ) return TCL_ERROR;
+ res = sqlite3_recover_config(pTest->p,
+ SQLITE_RECOVER_SLOWINDEXES, (void*)&iVal
+ );
+ break;
+ }
+ case 5: {
+ res = sqlite3_recover_config(pTest->p, 12345, 0);
+ break;
+ }
+ }
+ Tcl_SetObjResult(interp, Tcl_NewIntObj(res));
+ break;
+ }
+ case 1: assert( sqlite3_stricmp("run", aSub[iSub].zSub)==0 ); {
+ int res = sqlite3_recover_run(pTest->p);
+ Tcl_SetObjResult(interp, Tcl_NewIntObj(res));
+ break;
+ }
+ case 2: assert( sqlite3_stricmp("errmsg", aSub[iSub].zSub)==0 ); {
+ const char *zErr = sqlite3_recover_errmsg(pTest->p);
+ Tcl_SetObjResult(interp, Tcl_NewStringObj(zErr, -1));
+ break;
+ }
+ case 3: assert( sqlite3_stricmp("errcode", aSub[iSub].zSub)==0 ); {
+ int errCode = sqlite3_recover_errcode(pTest->p);
+ Tcl_SetObjResult(interp, Tcl_NewIntObj(errCode));
+ break;
+ }
+ case 4: assert( sqlite3_stricmp("finish", aSub[iSub].zSub)==0 ); {
+ int res = sqlite3_recover_errcode(pTest->p);
+ int res2;
+ if( res!=SQLITE_OK ){
+ const char *zErr = sqlite3_recover_errmsg(pTest->p);
+ Tcl_SetObjResult(interp, Tcl_NewStringObj(zErr, -1));
+ }
+ res2 = sqlite3_recover_finish(pTest->p);
+ assert( res2==res );
+ if( res ) return TCL_ERROR;
+ break;
+ }
+ case 5: assert( sqlite3_stricmp("step", aSub[iSub].zSub)==0 ); {
+ int res = sqlite3_recover_step(pTest->p);
+ Tcl_SetObjResult(interp, Tcl_NewIntObj(res));
+ break;
+ }
+ }
+
+ return TCL_OK;
+}
+
+/*
+** sqlite3_recover_init DB DBNAME URI
+*/
+static int test_sqlite3_recover_init(
+ void *clientData,
+ Tcl_Interp *interp,
+ int objc,
+ Tcl_Obj *CONST objv[]
+){
+ static int iTestRecoverCmd = 1;
+
+ TestRecover *pNew = 0;
+ sqlite3 *db = 0;
+ const char *zDb = 0;
+ const char *zUri = 0;
+ char zCmd[128];
+ int bSql = clientData ? 1 : 0;
+
+ if( objc!=4 ){
+ const char *zErr = (bSql ? "DB DBNAME SCRIPT" : "DB DBNAME URI");
+ Tcl_WrongNumArgs(interp, 1, objv, zErr);
+ return TCL_ERROR;
+ }
+ if( getDbPointer(interp, objv[1], &db) ) return TCL_ERROR;
+ zDb = Tcl_GetString(objv[2]);
+ if( zDb[0]=='\0' ) zDb = 0;
+
+ pNew = ckalloc(sizeof(TestRecover));
+ if( bSql==0 ){
+ zUri = Tcl_GetString(objv[3]);
+ pNew->p = sqlite3_recover_init(db, zDb, zUri);
+ }else{
+ pNew->interp = interp;
+ pNew->pScript = objv[3];
+ Tcl_IncrRefCount(pNew->pScript);
+ pNew->p = sqlite3_recover_init_sql(db, zDb, xSqlCallback, (void*)pNew);
+ }
+
+ sprintf(zCmd, "sqlite_recover%d", iTestRecoverCmd++);
+ Tcl_CreateObjCommand(interp, zCmd, testRecoverCmd, (void*)pNew, 0);
+
+ Tcl_SetObjResult(interp, Tcl_NewStringObj(zCmd, -1));
+ return TCL_OK;
+}
+
+/*
+** Declaration for public API function in file dbdata.c. This may be called
+** with NULL as the final two arguments to register the sqlite_dbptr and
+** sqlite_dbdata virtual tables with a database handle.
+*/
+#ifdef _WIN32
+__declspec(dllexport)
+#endif
+int sqlite3_dbdata_init(sqlite3*, char**, const sqlite3_api_routines*);
+
+/*
+** sqlite3_recover_init DB DBNAME URI
+*/
+static int test_sqlite3_dbdata_init(
+ void *clientData,
+ Tcl_Interp *interp,
+ int objc,
+ Tcl_Obj *CONST objv[]
+){
+ sqlite3 *db = 0;
+
+ if( objc!=2 ){
+ Tcl_WrongNumArgs(interp, 1, objv, "DB");
+ return TCL_ERROR;
+ }
+ if( getDbPointer(interp, objv[1], &db) ) return TCL_ERROR;
+ sqlite3_dbdata_init(db, 0, 0);
+
+ Tcl_ResetResult(interp);
+ return TCL_OK;
+}
+
+#endif /* SQLITE_OMIT_VIRTUALTABLE */
+
+int TestRecover_Init(Tcl_Interp *interp){
+#ifndef SQLITE_OMIT_VIRTUALTABLE
+ struct Cmd {
+ const char *zCmd;
+ Tcl_ObjCmdProc *xProc;
+ void *pArg;
+ } aCmd[] = {
+ { "sqlite3_recover_init", test_sqlite3_recover_init, 0 },
+ { "sqlite3_recover_init_sql", test_sqlite3_recover_init, (void*)1 },
+ { "sqlite3_dbdata_init", test_sqlite3_dbdata_init, (void*)1 },
+ };
+ int i;
+
+ for(i=0; i<sizeof(aCmd)/sizeof(struct Cmd); i++){
+ struct Cmd *p = &aCmd[i];
+ Tcl_CreateObjCommand(interp, p->zCmd, p->xProc, p->pArg, 0);
+ }
+#endif
+ return TCL_OK;
+}
+