/**
* SqlJetMemPage.java
* Copyright (C) 2009-2010 TMate Software Ltd
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* For information on how to redistribute this software under
* the terms of a license other than GNU General Public License
* contact TMate Software at support@sqljet.com
*/
package org.tmatesoft.sqljet.core.internal.btree;
import static org.tmatesoft.sqljet.core.internal.SqlJetUtility.get2byte;
import static org.tmatesoft.sqljet.core.internal.SqlJetUtility.get4byte;
import static org.tmatesoft.sqljet.core.internal.SqlJetUtility.getUnsignedByte;
import static org.tmatesoft.sqljet.core.internal.SqlJetUtility.getVarint;
import static org.tmatesoft.sqljet.core.internal.SqlJetUtility.getVarint32;
import static org.tmatesoft.sqljet.core.internal.SqlJetUtility.memcpy;
import static org.tmatesoft.sqljet.core.internal.SqlJetUtility.memset;
import static org.tmatesoft.sqljet.core.internal.SqlJetUtility.movePtr;
import static org.tmatesoft.sqljet.core.internal.SqlJetUtility.pointer;
import static org.tmatesoft.sqljet.core.internal.SqlJetUtility.put2byte;
import static org.tmatesoft.sqljet.core.internal.SqlJetUtility.put4byte;
import static org.tmatesoft.sqljet.core.internal.SqlJetUtility.putUnsignedByte;
import static org.tmatesoft.sqljet.core.internal.SqlJetUtility.putVarint;
import static org.tmatesoft.sqljet.core.internal.btree.SqlJetBtree.TRACE;
import org.tmatesoft.sqljet.core.SqlJetErrorCode;
import org.tmatesoft.sqljet.core.SqlJetException;
import org.tmatesoft.sqljet.core.internal.ISqlJetConfig;
import org.tmatesoft.sqljet.core.internal.ISqlJetLimits;
import org.tmatesoft.sqljet.core.internal.ISqlJetMemoryPointer;
import org.tmatesoft.sqljet.core.internal.ISqlJetPage;
import org.tmatesoft.sqljet.core.internal.SqlJetCloneable;
import org.tmatesoft.sqljet.core.internal.SqlJetUtility;
/**
* As each page of the file is loaded into memory, an instance of the following
* structure is appended and initialized to zero. This structure stores
* information about the page that is decoded from the raw file page.
*
* The pParent field points back to the parent page. This allows us to walk up
* the BTree from any leaf to the root. Care must be taken to unref() the parent
* page pointer when this page is no longer referenced. The pageDestructor()
* routine handles that chore.
*
* Access to all fields of this structure is controlled by the mutex stored in
* MemPage.pBt->mutex.
*
* @author TMate Software Ltd.
* @author Sergey Scherbina (sergey.scherbina@gmail.com)
*
*/
public class SqlJetMemPage extends SqlJetCloneable {
/**
* Page type flags. An ORed combination of these flags appear as the first
* byte of on-disk image of every BTree page.
*/
public static final byte PTF_INTKEY = 0x01;
public static final byte PTF_ZERODATA = 0x02;
public static final byte PTF_LEAFDATA = 0x04;
public static final byte PTF_LEAF = 0x08;
/** True if previously initialized. MUST BE FIRST! */
boolean isInit;
/** Number of overflow cell bodies in aCell[] */
int nOverflow;
/** True if intkey flag is set */
boolean intKey;
/** True if leaf flag is set */
boolean leaf;
/** True if this page stores data */
boolean hasData;
/** 100 for page 1. 0 otherwise */
byte hdrOffset;
/** 0 if leaf==1. 4 if leaf==0 */
byte childPtrSize;
/** Copy of BtShared.maxLocal or BtShared.maxLeaf */
int maxLocal;
/** Copy of BtShared.minLocal or BtShared.minLeaf */
int minLocal;
/** Index in aData of first cell pointer */
int cellOffset;
/** Number of free bytes on the page */
int nFree;
/** Number of cells on this page, local and ovfl */
int nCell;
/** Mask for page offset */
int maskPage;
static class _OvflCell extends SqlJetCloneable {
/** Pointers to the body of the overflow cell */
ISqlJetMemoryPointer pCell;
/** Insert this cell before idx-th non-overflow cell */
int idx;
}
/** Cells that will not fit on aData[] */
_OvflCell[] aOvfl = new _OvflCell[] { new _OvflCell(), new _OvflCell(), new _OvflCell(), new _OvflCell(),
new _OvflCell() };
/** Pointer to BtShared that this page is part of */
SqlJetBtreeShared pBt;
/** Pointer to disk image of the page data */
ISqlJetMemoryPointer aData;
/** Pager page handle */
ISqlJetPage pDbPage;
/** Page number for this page */
int pgno;
/**
* The ISAUTOVACUUM macro is used within balance_nonroot() to determine if
* the database supports auto-vacuum or not. Because it is used within an
* expression that is an argument to another macro (sqliteMallocRaw), it is
* not possible to use conditional compilation. So, this macro is defined
* instead.
*
* @return
*/
private boolean ISAUTOVACUUM() {
return pBt.autoVacuum;
}
/**
* Decode the flags byte (the first byte of the header) for a page and
* initialize fields of the MemPage structure accordingly.
*
* Only the following combinations are supported. Anything different
* indicates a corrupt database files:
*
* <p>
* PTF_ZERODATA
* </p>
* <p>
* PTF_ZERODATA | PTF_LEAF
* </p>
* <p>
* PTF_LEAFDATA | PTF_INTKEY
* </p>
* <p>
* PTF_LEAFDATA | PTF_INTKEY | PTF_LEAF
* </p>
*/
public void decodeFlags(int flagByte) throws SqlJetException {
assert (hdrOffset == (pgno == 1 ? 100 : 0));
assert (pBt.mutex.held());
leaf = (flagByte >> 3) > 0;
flagByte &= ~PTF_LEAF;
childPtrSize = (byte) (4 - 4 * (leaf ? 1 : 0));
if (flagByte == (PTF_LEAFDATA | PTF_INTKEY)) {
intKey = true;
hasData = leaf;
maxLocal = pBt.maxLeaf;
minLocal = pBt.minLeaf;
} else if (flagByte == PTF_ZERODATA) {
intKey = false;
hasData = false;
maxLocal = pBt.maxLocal;
minLocal = pBt.minLocal;
} else {
throw new SqlJetException(SqlJetErrorCode.CORRUPT);
}
return;
}
/**
* Initialize the auxiliary information for a disk block.
*
* Return SQLITE_OK on success. If we see that the page does not contain a
* well-formed database page, then return SQLITE_CORRUPT. Note that a return
* of SQLITE_OK does not guarantee that the page is well-formed. It only
* shows that we failed to detect any corruption.
*/
public void initPage() throws SqlJetException {
assert (pBt != null);
assert (pBt.mutex.held());
assert (pgno == pDbPage.getPageNumber());
assert (this == pDbPage.getExtra());
assert (aData.getBuffer() == pDbPage.getData().getBuffer());
if (!isInit) {
int pc; /* Address of a freeblock within pPage->aData[] */
byte hdr; /* Offset to beginning of page header */
int usableSize; /* Amount of usable space on each page */
int cellOffset; /* Offset from start of page to first cell pointer */
int nFree; /* Number of unused bytes on the page */
int top; /* First byte of the cell content area */
hdr = hdrOffset;
decodeFlags(SqlJetUtility.getUnsignedByte(aData, hdr));
assert (pBt.pageSize >= 512 && pBt.pageSize <= 32768);
maskPage = pBt.pageSize - 1;
nOverflow = 0;
usableSize = pBt.usableSize;
this.cellOffset = cellOffset = hdr + 12 - 4 * (leaf ? 1 : 0);
top = get2byte(aData, hdr + 5);
nCell = get2byte(aData, hdr + 3);
if (nCell > pBt.MX_CELL()) {
/* To many cells for a single page. The page must be corrupt */
throw new SqlJetException(SqlJetErrorCode.CORRUPT);
}
/* Compute the total free space on the page */
pc = get2byte(aData, hdr + 1);
nFree = SqlJetUtility.getUnsignedByte(aData, hdr + 7) + top - (cellOffset + 2 * nCell);
while (pc > 0) {
int next, size;
if (pc > usableSize - 4) {
/* Free block is off the page */
throw new SqlJetException(SqlJetErrorCode.CORRUPT);
}
next = get2byte(aData, pc);
size = get2byte(aData, pc + 2);
if (next > 0 && next <= pc + size + 3) {
/* Free blocks must be in accending order */
throw new SqlJetException(SqlJetErrorCode.CORRUPT);
}
nFree += size;
pc = next;
}
this.nFree = nFree;
if (nFree >= usableSize) {
/* Free space cannot exceed total page size */
throw new SqlJetException(SqlJetErrorCode.CORRUPT);
}
isInit = true;
}
}
/**
* Release a MemPage. This should be called once for each prior call to
* sqlite3BtreeGetPage.
*
* @throws SqlJetException
*/
public static void releasePage(SqlJetMemPage pPage) throws SqlJetException {
if (pPage != null) {
assert (pPage.nOverflow == 0 || pPage.pDbPage.getRefCount() > 1);
assert (pPage.aData != null);
assert (pPage.pBt != null);
assert (pPage.pDbPage.getExtra() == pPage);
assert (pPage.pDbPage.getData().getBuffer() == pPage.aData.getBuffer());
assert (pPage.pBt.mutex.held());
pPage.pDbPage.unref();
}
}
/**
* Set the pointer-map entries for all children of page pPage. Also, if
* pPage contains cells that point to overflow pages, set the pointer map
* entries for the overflow pages as well.
*
* @throws SqlJetException
*/
public void setChildPtrmaps() throws SqlJetException {
int i; /* Counter variable */
int nCell; /* Number of cells in page pPage */
boolean isInitOrig = isInit;
assert (pBt.mutex.held());
try {
initPage();
nCell = this.nCell;
for (i = 0; i < nCell; i++) {
ISqlJetMemoryPointer pCell = findCell(i);
ptrmapPutOvflPtr(pCell);
if (!leaf) {
int childPgno = get4byte(pCell);
pBt.ptrmapPut(childPgno, SqlJetBtreeShared.PTRMAP_BTREE, pgno);
}
}
if (!leaf) {
int childPgno = get4byte(aData, hdrOffset + 8);
pBt.ptrmapPut(childPgno, SqlJetBtreeShared.PTRMAP_BTREE, pgno);
}
} catch (SqlJetException e) {
// set_child_ptrmaps_out:
isInit = isInitOrig;
throw e;
}
}
/**
* Somewhere on pPage, which is guarenteed to be a btree page, not an
* overflow page, is a pointer to page iFrom. Modify this pointer so that it
* points to iTo. Parameter eType describes the type of pointer to be
* modified, as follows:
*
* PTRMAP_BTREE: pPage is a btree-page. The pointer points at a child page
* of pPage.
*
* PTRMAP_OVERFLOW1: pPage is a btree-page. The pointer points at an
* overflow page pointed to by one of the cells on pPage.
*
* PTRMAP_OVERFLOW2: pPage is an overflow-page. The pointer points at the
* next overflow page in the list.
*
* @throws SqlJetExceptionRemove
*/
public void modifyPagePointer(int iFrom, int iTo, short s) throws SqlJetException {
assert (pBt.mutex.held());
if (s == SqlJetBtreeShared.PTRMAP_OVERFLOW2) {
/* The pointer is always the first 4 bytes of the page in this case. */
if (get4byte(aData) != iFrom) {
throw new SqlJetException(SqlJetErrorCode.CORRUPT);
}
put4byte(aData, iTo);
} else {
boolean isInitOrig = this.isInit;
int i;
int nCell;
initPage();
nCell = this.nCell;
for (i = 0; i < nCell; i++) {
ISqlJetMemoryPointer pCell = findCell(i);
if (s == SqlJetBtreeShared.PTRMAP_OVERFLOW1) {
SqlJetBtreeCellInfo info;
info = parseCellPtr(pCell);
if (info.iOverflow > 0) {
if (iFrom == get4byte(pCell, info.iOverflow)) {
put4byte(pCell, info.iOverflow, iTo);
break;
}
}
} else {
if (get4byte(pCell) == iFrom) {
put4byte(pCell, iTo);
break;
}
}
}
if (i == nCell) {
if (s != SqlJetBtreeShared.PTRMAP_BTREE || get4byte(aData, hdrOffset + 8) != iFrom) {
throw new SqlJetException(SqlJetErrorCode.CORRUPT);
}
put4byte(aData, hdrOffset + 8, iTo);
}
this.isInit = isInitOrig;
}
}
/**
* Given a btree page and a cell index (0 means the first cell on the page,
* 1 means the second cell, and so forth) return a pointer to the cell
* content.
*
* This routine works only for pages that do not contain overflow cells.
*/
public ISqlJetMemoryPointer findCell(int i) {
return pointer(aData, maskPage & get2byte(aData, cellOffset + 2 * i));
}
/**
* If the cell pCell, part of page pPage contains a pointer to an overflow
* page, insert an entry into the pointer-map for the overflow page.
*
* @throws SqlJetException
*/
private void ptrmapPutOvflPtr(ISqlJetMemoryPointer pCell) throws SqlJetException {
assert (pCell != null);
SqlJetBtreeCellInfo info = parseCellPtr(pCell);
assert ((info.nData + (intKey ? 0 : info.nKey)) == info.nPayload);
if ((info.nData + (intKey ? 0 : info.nKey)) > info.nLocal) {
int ovfl = get4byte(pCell, info.iOverflow);
pBt.ptrmapPut(ovfl, SqlJetBtreeShared.PTRMAP_OVERFLOW1, pgno);
}
}
/**
* Parse a cell content block and fill in the CellInfo structure. There are
* two versions of this function. sqlite3BtreeParseCell() takes a cell index
* as the second argument and sqlite3BtreeParseCellPtr() takes a pointer to
* the body of the cell as its second argument.
*
* @param pCell
* Pointer to the cell text.
* @return
*/
SqlJetBtreeCellInfo parseCellPtr(ISqlJetMemoryPointer pCell) {
int n; /* Number bytes in cell content header */
int[] nPayload = new int[1]; /* Number of bytes of cell payload */
assert (pBt.mutex.held());
SqlJetBtreeCellInfo pInfo = new SqlJetBtreeCellInfo();
pInfo.pCell = pCell;
n = childPtrSize;
assert (n == 4 - 4 * (leaf ? 1 : 0));
if (intKey) {
if (hasData) {
n += getVarint32(pCell, n, nPayload);
} else {
nPayload[0] = 0;
}
long[] pInfo_nKey = new long[1];
n += getVarint(pCell, n, pInfo_nKey);
pInfo.nKey = pInfo_nKey[0];
pInfo.nData = nPayload[0];
} else {
pInfo.nData = 0;
n += getVarint32(pCell, n, nPayload);
pInfo.nKey = nPayload[0];
}
pInfo.nPayload = nPayload[0];
pInfo.nHeader = n;
if (nPayload[0] <= this.maxLocal) {
/*
* This is the (easy) common case where the entire payload fits on
* the local page. No overflow is required.
*/
int nSize; /* Total size of cell content in bytes */
nSize = nPayload[0] + n;
pInfo.nLocal = nPayload[0];
pInfo.iOverflow = 0;
if ((nSize & ~3) == 0) {
nSize = 4; /* Minimum cell size is 4 */
}
pInfo.nSize = nSize;
} else {
/*
* If the payload will not fit completely on the local page, we have
* to decide how much to store locally and how much to spill onto
* overflow pages. The strategy is to minimize the amount of unused
* space on overflow pages while keeping the amount of local storage
* in between minLocal and maxLocal.
*
* Warning: changing the way overflow payload is distributed in any
* way will result in an incompatible file format.
*/
int minLocal; /* Minimum amount of payload held locally */
int maxLocal; /* Maximum amount of payload held locally */
int surplus; /* Overflow payload available for local storage */
minLocal = this.minLocal;
maxLocal = this.maxLocal;
surplus = minLocal + (nPayload[0] - minLocal) % (pBt.usableSize - 4);
if (surplus <= maxLocal) {
pInfo.nLocal = surplus;
} else {
pInfo.nLocal = minLocal;
}
pInfo.iOverflow = pInfo.nLocal + n;
pInfo.nSize = pInfo.iOverflow + 4;
}
return pInfo;
}
/**
* @param iCell
* The cell index. First cell is 0
* @return
*/
public SqlJetBtreeCellInfo parseCell(int iCell) {
return parseCellPtr(findCell(iCell));
}
/**
* Set up a raw page so that it looks like a database page holding no
* entries.
*
* @param sqlJetBtree
* @param flags
* @throws SqlJetException
*/
void zeroPage(int flags) throws SqlJetException {
ISqlJetMemoryPointer data = aData;
byte hdr = hdrOffset;
int first;
assert (pDbPage.getPageNumber() == pgno);
assert (pDbPage.getExtra() == this);
assert (pDbPage.getData().getBuffer() == data.getBuffer());
assert (pBt.mutex.held());
SqlJetUtility.putUnsignedByte(data, hdr, (short) flags);
first = hdr + 8 + 4 * ((flags & SqlJetMemPage.PTF_LEAF) == 0 ? 1 : 0);
// SqlJetUtility.memset(data, hdr + 1, (byte) 0, 4);
SqlJetUtility.put4byte(data, hdr + 1, 0);
//
SqlJetUtility.putUnsignedByte(data, hdr + 7, (short) 0);
SqlJetUtility.put2byte(data, hdr + 5, pBt.usableSize);
nFree = pBt.usableSize - first;
decodeFlags(flags);
hdrOffset = hdr;
cellOffset = first;
nOverflow = 0;
assert (pBt.pageSize >= 512 && pBt.pageSize <= 32768);
maskPage = pBt.pageSize - 1;
nCell = 0;
isInit = true;
}
/**
* Add a page of the database file to the freelist. unref() is NOT called
* for pPage.
*/
public void freePage() throws SqlJetException {
SqlJetMemPage pPage1 = pBt.pPage1;
int n, k;
/* Prepare the page for freeing */
assert (pBt.mutex.held());
assert (this.pgno > 1);
this.isInit = false;
/* Increment the free page count on pPage1 */
pPage1.pDbPage.write();
n = get4byte(pPage1.aData, 36);
put4byte(pPage1.aData, 36, n + 1);
if (ISqlJetConfig.SECURE_DELETE) {
/*
* If the SQLITE_SECURE_DELETE compile-time option is enabled, then
* always fully overwrite deleted information with zeros.
*/
pDbPage.write();
memset(aData, (byte) 0, pBt.pageSize);
}
/*
* If the database supports auto-vacuum, write an entry in the
* pointer-map to indicate that the page is free.
*/
if (ISAUTOVACUUM()) {
pBt.ptrmapPut(pgno, SqlJetBtreeShared.PTRMAP_FREEPAGE, 0);
}
if (n == 0) {
/* This is the first free page */
pDbPage.write();
memset(aData, (byte) 0, 8);
put4byte(pPage1.aData, 32, pgno);
TRACE("FREE-PAGE: %d first\n", this.pgno);
} else {
/*
* Other free pages already exist. Retrive the first trunk page* of
* the freelist and find out how many leaves it has.
*/
SqlJetMemPage pTrunk;
pTrunk = pBt.getPage(get4byte(pPage1.aData, 32), false);
k = get4byte(pTrunk.aData, 4);
if (k >= pBt.usableSize / 4 - 8) {
/*
* The trunk is full. Turn the page being freed into a new*
* trunk page with no leaves.** Note that the trunk page is not
* really full until it contains* usableSize/4 - 2 entries, not
* usableSize/4 - 8 entries as we have* coded. But due to a
* coding error in versions of SQLite prior to* 3.6.0, databases
* with freelist trunk pages holding more than* usableSize/4 - 8
* entries will be reported as corrupt. In order* to maintain
* backwards compatibility with older versions of SQLite,* we
* will contain to restrict the number of entries to
* usableSize/4 - 8* for now. At some point in the future (once
* everyone has upgraded* to 3.6.0 or later) we should consider
* fixing the conditional above* to read "usableSize/4-2"
* instead of "usableSize/4-8".
*/
pDbPage.write();
put4byte(aData, pTrunk.pgno);
put4byte(aData, 4, 0);
put4byte(pPage1.aData, 32, pgno);
TRACE("FREE-PAGE: %d new trunk page replacing %d\n", this.pgno, pTrunk.pgno);
} else if (k < 0) {
throw new SqlJetException(SqlJetErrorCode.CORRUPT);
} else {
/* Add the newly freed page as a leaf on the current trunk */
pTrunk.pDbPage.write();
put4byte(pTrunk.aData, 4, k + 1);
put4byte(pTrunk.aData, 8 + k * 4, pgno);
if (ISqlJetConfig.SECURE_DELETE) {
pDbPage.dontWrite();
}
TRACE("FREE-PAGE: %d leaf on trunk page %d\n", this.pgno, pTrunk.pgno);
}
releasePage(pTrunk);
}
}
/**
** Free any overflow pages associated with the given Cell.
*/
public void clearCell(ISqlJetMemoryPointer pCell) throws SqlJetException {
SqlJetBtreeCellInfo info;
int[] ovflPgno = new int[1];
int nOvfl;
int ovflPageSize;
assert (pBt.mutex.held());
info = parseCellPtr(pCell);
if (info.iOverflow == 0) {
return; /* No overflow pages. Return without doing anything */
}
ovflPgno[0] = get4byte(pCell, info.iOverflow);
ovflPageSize = pBt.usableSize - 4;
nOvfl = (info.nPayload - info.nLocal + ovflPageSize - 1) / ovflPageSize;
assert (ovflPgno[0] == 0 || nOvfl > 0);
while (nOvfl-- != 0) {
SqlJetMemPage[] pOvfl = new SqlJetMemPage[1];
if (ovflPgno[0] <2 || ovflPgno[0] > pBt.pPager.getPageCount()) {
/* 0 is not a legal page number and page 1 cannot be an
** overflow page. Therefore if ovflPgno<2 or past the end of the
** file the database must be corrupt. */
throw new SqlJetException(SqlJetErrorCode.CORRUPT);
}
pBt.getOverflowPage(ovflPgno[0], pOvfl, (nOvfl == 0) ? null : ovflPgno);
pOvfl[0].freePage();
pOvfl[0].pDbPage.unref();
}
}
/**
** Compute the total number of bytes that a Cell needs in the cell data area
* of the btree-page. The return number includes the cell data header and
* the local payload, but not any overflow page or the space used by the
* cell pointer.
*/
int cellSize(int iCell) {
SqlJetBtreeCellInfo info = parseCell(iCell);
return info.nSize;
}
int cellSizePtr(ISqlJetMemoryPointer pCell) {
SqlJetBtreeCellInfo info = parseCellPtr(pCell);
return info.nSize;
}
/**
* Remove the i-th cell from pPage. This routine effects pPage only. The
* cell content is not freed or deallocated. It is assumed that the cell
* content has been copied someplace else. This routine just removes the
* reference to the cell from pPage.
*
* "sz" must be the number of bytes in the cell.
*
* @param idx
* @param sz
* @throws SqlJetException
*/
public void dropCell(int idx, int sz) throws SqlJetException {
final SqlJetMemPage pPage = this;
int i; /* Loop counter */
int pc; /* Offset to cell content of cell being deleted */
ISqlJetMemoryPointer data; /* pPage->aData */
ISqlJetMemoryPointer ptr; /* Used to move bytes around within data[] */
assert (idx >= 0 && idx < pPage.nCell);
assert (sz == pPage.cellSize(idx));
assert (pPage.pBt.mutex.held());
data = pPage.aData;
ptr = pointer(data, pPage.cellOffset + 2 * idx);
pc = get2byte(ptr);
if ((pc < pPage.hdrOffset + 6 + (pPage.leaf ? 0 : 4)) || (pc + sz > pPage.pBt.usableSize)) {
throw new SqlJetException(SqlJetErrorCode.CORRUPT);
}
pPage.freeSpace(pc, sz);
for (i = idx + 1; i < pPage.nCell; i++, movePtr(ptr, 2)) {
putUnsignedByte(ptr, 0, getUnsignedByte(ptr, 2));
putUnsignedByte(ptr, 1, getUnsignedByte(ptr, 3));
}
pPage.nCell--;
put2byte(data, pPage.hdrOffset + 3, pPage.nCell);
pPage.nFree += 2;
}
/*
* * Return a section of the pPage->aData to the freelist.* The first byte
* of the new free block is pPage->aDisk[start]* and the size of the block
* is "size" bytes.** Most of the effort here is involved in coalesing
* adjacent* free blocks into a single big free block.
*/
private void freeSpace(int start, int size) throws SqlJetException {
SqlJetMemPage pPage = this;
int addr, pbegin, hdr;
ISqlJetMemoryPointer data = pPage.aData;
assert (pPage.pBt != null);
assert (start >= pPage.hdrOffset + 6 + (pPage.leaf ? 0 : 4));
assert ((start + size) <= pPage.pBt.usableSize);
assert (pPage.pBt.mutex.held());
assert (size >= 0); /* Minimum cell size is 4 */
if (ISqlJetConfig.SECURE_DELETE) {
/*
* Overwrite deleted information with zeros when the SECURE_DELETE*
* option is enabled at compile-time
*/
memset(data, start, (byte) 0, size);
}
/* Add the space back into the linked list of freeblocks */
hdr = pPage.hdrOffset;
addr = hdr + 1;
while ((pbegin = get2byte(data, addr)) < start && pbegin > 0) {
assert (pbegin <= pPage.pBt.usableSize - 4);
if (pbegin <= addr) {
throw new SqlJetException(SqlJetErrorCode.CORRUPT);
}
addr = pbegin;
}
if (pbegin > pPage.pBt.usableSize - 4) {
throw new SqlJetException(SqlJetErrorCode.CORRUPT);
}
assert (pbegin > addr || pbegin == 0);
put2byte(data, addr, start);
put2byte(data, start, pbegin);
put2byte(data, start + 2, size);
pPage.nFree += size;
/* Coalesce adjacent free blocks */
addr = pPage.hdrOffset + 1;
while ((pbegin = get2byte(data, addr)) > 0) {
int pnext, psize, x;
assert (pbegin > addr);
assert (pbegin <= pPage.pBt.usableSize - 4);
pnext = get2byte(data, pbegin);
psize = get2byte(data, pbegin + 2);
if (pbegin + psize + 3 >= pnext && pnext > 0) {
int frag = pnext - (pbegin + psize);
if ((frag < 0) || (frag > (int) SqlJetUtility.getUnsignedByte(data, pPage.hdrOffset + 7))) {
throw new SqlJetException(SqlJetErrorCode.CORRUPT);
}
SqlJetUtility.putUnsignedByte(data, pPage.hdrOffset + 7, (byte) (SqlJetUtility.getUnsignedByte(data,
pPage.hdrOffset + 7) - (byte) frag));
x = get2byte(data, pnext);
put2byte(data, pbegin, x);
x = pnext + get2byte(data, pnext + 2) - pbegin;
put2byte(data, pbegin + 2, x);
} else {
addr = pbegin;
}
}
/* If the cell content area begins with a freeblock, remove it. */
if (SqlJetUtility.getUnsignedByte(data, hdr + 1) == SqlJetUtility.getUnsignedByte(data, hdr + 5)
&& SqlJetUtility.getUnsignedByte(data, hdr + 2) == SqlJetUtility.getUnsignedByte(data, hdr + 6)) {
int top;
pbegin = get2byte(data, hdr + 1);
memcpy(data, hdr + 1, data, pbegin, 2);
top = get2byte(data, hdr + 5) + get2byte(data, pbegin + 2);
put2byte(data, hdr + 5, top);
}
assert (pPage.pDbPage.isWriteable());
}
/**
* Insert a new cell on pPage at cell index "i". pCell points to the content
* of the cell.
*
* If the cell content will fit on the page, then put it there. If it will
* not fit, then make a copy of the cell content into pTemp if pTemp is not
* null. Regardless of pTemp, allocate a new entry in pPage->aOvfl[] and
* make it point to the cell content (either in pTemp or the original pCell)
* and also record its index. Allocating a new entry in pPage->aCell[]
* implies that pPage->nOverflow is incremented.
*
* If nSkip is non-zero, then do not copy the first nSkip bytes of the cell.
* The caller will overwrite them after this function returns. If nSkip is
* non-zero, then pCell may not point to an invalid memory location (but
* pCell+nSkip is always valid).
*
* @param i
* New cell becomes the i-th cell of the page
* @param pCell
* Content of the new cell
* @param sz
* Bytes of content in pCell
* @param pTemp
* Temp storage space for pCell, if needed
* @param nSkip
* Do not write the first nSkip bytes of the cell
*
* @throws SqlJetException
*/
public void insertCell(int i, ISqlJetMemoryPointer pCell, int sz, ISqlJetMemoryPointer pTemp, byte nSkip)
throws SqlJetException {
final SqlJetMemPage pPage = this;
int idx; /* Where to write new cell content in data[] */
int j; /* Loop counter */
int top; /* First byte of content for any cell in data[] */
int end; /* First byte past the last cell pointer in data[] */
int ins; /* Index in data[] where new cell pointer is inserted */
int hdr; /* Offset into data[] of the page header */
int cellOffset; /* Address of first cell pointer in data[] */
ISqlJetMemoryPointer data; /* The content of the whole page */
assert (i >= 0 && i <= pPage.nCell + pPage.nOverflow);
assert (pPage.nCell <= pPage.pBt.MX_CELL() && pPage.pBt.MX_CELL() <= 5460);
assert (pPage.nOverflow <= pPage.aOvfl.length);
assert (sz == pPage.cellSizePtr(pCell));
assert (pPage.pBt.mutex.held());
if (pPage.nOverflow != 0 || sz + 2 > pPage.nFree) {
if (pTemp != null) {
memcpy(pTemp, nSkip, pCell, nSkip, sz - nSkip);
pCell = pTemp;
}
j = pPage.nOverflow++;
// assert( j<(int)(sizeof(pPage.aOvfl)/sizeof(pPage.aOvfl[0])) );
pPage.aOvfl[j].pCell = pCell;
pPage.aOvfl[j].idx = i;
pPage.nFree = 0;
} else {
pPage.pDbPage.write();
assert (pPage.pDbPage.isWriteable());
data = pPage.aData;
hdr = pPage.hdrOffset;
top = get2byte(data, hdr + 5);
cellOffset = pPage.cellOffset;
end = cellOffset + 2 * pPage.nCell + 2;
ins = cellOffset + 2 * i;
if (end > top - sz) {
pPage.defragmentPage();
top = get2byte(data, hdr + 5);
assert (end + sz <= top);
}
idx = pPage.allocateSpace(sz);
assert (idx > 0);
assert (end <= get2byte(data, hdr + 5));
if (idx + sz > pPage.pBt.usableSize) {
throw new SqlJetException(SqlJetErrorCode.CORRUPT);
}
pPage.nCell++;
pPage.nFree -= 2;
memcpy(data, idx + nSkip, pCell, nSkip, sz - nSkip);
for (j = end - 2; j > ins; j -= 2) {
SqlJetUtility.putUnsignedByte(data, j, SqlJetUtility.getUnsignedByte(data, j - 2));
SqlJetUtility.putUnsignedByte(data, j + 1, SqlJetUtility.getUnsignedByte(data, j - 1));
}
put2byte(data, ins, idx);
put2byte(data, hdr + 3, pPage.nCell);
if (pPage.pBt.autoVacuum) {
/*
* The cell may contain a pointer to an overflow page. If so,
* write* the entry for the overflow page into the pointer map.
*/
SqlJetBtreeCellInfo info = pPage.parseCellPtr(pCell);
assert ((info.nData + (pPage.intKey ? 0 : info.nKey)) == info.nPayload);
if ((info.nData + (pPage.intKey ? 0 : info.nKey)) > info.nLocal) {
int pgnoOvfl = get4byte(pCell, info.iOverflow);
pPage.pBt.ptrmapPut(pgnoOvfl, SqlJetBtreeShared.PTRMAP_OVERFLOW1, pPage.pgno);
}
}
}
}
/**
* Allocate nByte bytes of space on a page.
*
* Return the index into pPage->aData[] of the first byte of the new
* allocation. The caller guarantees that there is enough space. This
* routine will never fail.
*
* If the page contains nBytes of free space but does not contain nBytes of
* contiguous free space, then this routine automatically calls
* defragementPage() to consolidate all free space before allocating the new
* chunk.
*
* @param nByte
* @return
*
* @throws SqlJetException
*/
private int allocateSpace(int nByte) throws SqlJetException {
final SqlJetMemPage pPage = this;
int addr, pc, hdr;
int size;
int nFrag;
int top;
int nCell;
int cellOffset;
ISqlJetMemoryPointer data;
data = pPage.aData;
assert (pPage.pDbPage.isWriteable());
assert (pPage.pBt != null);
assert (pPage.pBt.mutex.held());
assert (nByte >= 0); /* Minimum cell size is 4 */
assert (pPage.nFree >= nByte);
assert (pPage.nOverflow == 0);
hdr = pPage.hdrOffset;
nFrag = SqlJetUtility.getUnsignedByte(data, hdr + 7);
if (nFrag < 60) {
/*
* Search the freelist looking for a slot big enough to satisfy the*
* space request.
*/
addr = hdr + 1;
while ((pc = get2byte(data, addr)) > 0) {
size = get2byte(data, pc + 2);
if (size >= nByte) {
int x = size - nByte;
if (size < nByte + 4) {
memcpy(data, addr, data, pc, 2);
SqlJetUtility.putUnsignedByte(data, hdr + 7, (byte) (nFrag + x));
pPage.nFree -= nByte;
return pc;
} else {
put2byte(data, pc + 2, x);
pPage.nFree -= nByte;
return pc + x;
}
}
addr = pc;
}
}
/*
* Allocate memory from the gap in between the cell pointer array* and
* the cell content area.
*/
top = get2byte(data, hdr + 5);
nCell = get2byte(data, hdr + 3);
cellOffset = pPage.cellOffset;
if (nFrag >= 60 || cellOffset + 2 * nCell > top - nByte) {
defragmentPage();
top = get2byte(data, hdr + 5);
}
top -= nByte;
assert (cellOffset + 2 * nCell <= top);
put2byte(data, hdr + 5, top);
assert (pPage.pDbPage.isWriteable());
pPage.nFree -= nByte;
return top;
}
/**
* Defragment the page given. All Cells are moved to the end of the page and
* all free space is collected into one big FreeBlk that occurs in between
* the header and cell pointer array and the cell content area.
*
* @throws SqlJetException
*/
private void defragmentPage() throws SqlJetException {
final SqlJetMemPage pPage = this;
int i; /* Loop counter */
int pc; /* Address of a i-th cell */
int hdr; /* Offset to the page header */
int size; /* Size of a cell */
int usableSize; /* Number of usable bytes on a page */
int cellOffset; /* Offset to the cell pointer array */
int cbrk; /* Offset to the cell content area */
int nCell; /* Number of cells on the page */
ISqlJetMemoryPointer data; /* The page data */
ISqlJetMemoryPointer temp; /* Temp area for cell content */
int iCellFirst; /* First allowable cell index */
int iCellLast; /* Last possible cell index */
assert (pPage.pDbPage.isWriteable());
assert (pPage.pBt != null);
assert (pPage.pBt.usableSize <= ISqlJetLimits.SQLJET_MAX_PAGE_SIZE);
assert (pPage.nOverflow == 0);
assert (pPage.pBt.mutex.held());
temp = pPage.pBt.pPager.getTempSpace();
data = pPage.aData;
hdr = pPage.hdrOffset;
cellOffset = pPage.cellOffset;
nCell = pPage.nCell;
assert (nCell == get2byte(data, hdr + 3));
usableSize = pPage.pBt.usableSize;
cbrk = get2byte(data, hdr + 5);
memcpy(temp, cbrk, data, cbrk, usableSize - cbrk);
cbrk = usableSize;
iCellFirst = cellOffset + 2*nCell;
iCellLast = usableSize - 4;
for(i=0; i<nCell; i++){
final ISqlJetMemoryPointer pAddr = data.getBuffer().getPointer(cellOffset + i*2); /* The i-th cell pointer */
pc = get2byte(pAddr);
if( pc<iCellFirst || pc>iCellLast ){
throw new SqlJetException(SqlJetErrorCode.CORRUPT);
}
assert( pc>=iCellFirst && pc<=iCellLast );
size = pPage.cellSizePtr(temp.getBuffer().getPointer(pc));
cbrk -= size;
if( cbrk<iCellFirst || pc+size>usableSize ){
throw new SqlJetException(SqlJetErrorCode.CORRUPT);
}
assert( cbrk+size<=usableSize && cbrk>=iCellFirst );
memcpy(data, cbrk, temp, pc, size);
put2byte(pAddr, cbrk);
}
assert( cbrk>=iCellFirst );
put2byte(data, hdr + 5, cbrk);
SqlJetUtility.putUnsignedByte(data, hdr + 1, (byte) 0);
SqlJetUtility.putUnsignedByte(data, hdr + 2, (byte) 0);
SqlJetUtility.putUnsignedByte(data, hdr + 7, (byte) 0);
memset(data, iCellFirst, (byte) 0, cbrk - iCellFirst);
assert (pPage.pDbPage.isWriteable());
if (cbrk - iCellFirst != pPage.nFree) {
throw new SqlJetException(SqlJetErrorCode.CORRUPT);
}
}
/**
* This a more complex version of findCell() that works for pages that do
* contain overflow cells. See insert
*
* @param iCell
* @return
*/
public ISqlJetMemoryPointer findOverflowCell(int iCell) {
final SqlJetMemPage pPage = this;
int i;
assert (pPage.pBt.mutex.held());
for (i = pPage.nOverflow - 1; i >= 0; i--) {
int k;
_OvflCell pOvfl = pPage.aOvfl[i];
k = pOvfl.idx;
if (k <= iCell) {
if (k == iCell) {
return pOvfl.pCell;
}
iCell--;
}
}
return pPage.findCell(iCell);
}
/**
* Add a list of cells to a page. The page should be initially empty. The
* cells are guaranteed to fit on the page.
*
* @param nCell
* The number of cells to add to this page
* @param apCell
* Pointers to cell bodies
* @param aSize
* Sizes of the cells
*
* @throws SqlJetException
*/
public void assemblePage(int nCell, ISqlJetMemoryPointer[] apCell, int[] aSize) throws SqlJetException {
assemblePage(nCell, apCell, 0, aSize, 0);
}
public void assemblePage(int nCell, ISqlJetMemoryPointer[] apCell, int apCellPos, int[] aSize, int aSizePos)
throws SqlJetException {
final SqlJetMemPage pPage = this;
int i; /* Loop counter */
int totalSize; /* Total size of all cells */
int hdr; /* Index of page header */
int cellptr; /* Address of next cell pointer */
int cellbody; /* Address of next cell body */
ISqlJetMemoryPointer data; /* Data for the page */
assert (pPage.nOverflow == 0);
assert (pPage.pBt.mutex.held());
assert (nCell >= 0 && nCell <= pPage.pBt.MX_CELL() && pPage.pBt.MX_CELL() <= 5460);
totalSize = 0;
for (i = 0; i < nCell; i++) {
totalSize += aSize[aSizePos + i];
}
assert (totalSize + 2 * nCell <= pPage.nFree);
assert (pPage.nCell == 0);
assert (pPage.pDbPage.isWriteable());
cellptr = pPage.cellOffset;
data = pPage.aData;
hdr = pPage.hdrOffset;
put2byte(data, hdr + 3, nCell);
if (nCell != 0) {
cellbody = pPage.allocateSpace(totalSize);
assert (cellbody > 0);
assert (pPage.nFree >= 2 * nCell);
pPage.nFree -= 2 * nCell;
for (i = 0; i < nCell; i++) {
put2byte(data, cellptr, cellbody);
memcpy(data, cellbody, apCell[apCellPos + i], 0, aSize[aSizePos + i]);
cellptr += 2;
cellbody += aSize[aSizePos + i];
}
assert (cellbody == pPage.pBt.usableSize);
}
pPage.nCell = nCell;
}
/**
* Page pParent is an internal (non-leaf) tree page. This function asserts
* that page number iChild is the left-child if the iIdx'th cell in page
* pParent. Or, if iIdx is equal to the total number of cells in pParent,
* that page number iChild is the right-child of the page.
*
* @param iIdx
* @param iChild
*/
public void assertParentIndex(int iIdx, int iChild) {
final SqlJetMemPage pParent = this;
assert (iIdx <= pParent.nCell);
if (iIdx == pParent.nCell) {
assert (get4byte(pParent.aData, pParent.hdrOffset + 8) == iChild);
} else {
assert (get4byte(pParent.findCell(iIdx)) == iChild);
}
}
/**
* Create the byte sequence used to represent a cell on page pPage and write
* that byte sequence into pCell[]. Overflow pages are allocated and filled
* in as necessary. The calling procedure is responsible for making sure
* sufficient space has been allocated for pCell[].
*
* Note that pCell does not necessary need to point to the pPage->aData
* area. pCell might point to some temporary storage. The cell will be
* constructed in this temporary area then copied into pPage->aData later.
*
* @param pCell
* Complete text of the cell
* @param pKey
* The key
* @param nKey
* The key
* @param pData
* The data
* @param nData
* The data
* @param nZero
* Extra zero bytes to append to pData
*
* @return cell size
*
* @throws SqlJetException
*/
public int fillInCell(ISqlJetMemoryPointer pCell, ISqlJetMemoryPointer pKey, long nKey, ISqlJetMemoryPointer pData,
int nData, int nZero) throws SqlJetException {
final SqlJetMemPage pPage = this;
int pnSize = 0;
int nPayload;
ISqlJetMemoryPointer pSrc;
int nSrc, n;
int spaceLeft;
SqlJetMemPage pOvfl = null;
SqlJetMemPage pToRelease = null;
ISqlJetMemoryPointer pPrior;
ISqlJetMemoryPointer pPayload;
SqlJetBtreeShared pBt = pPage.pBt;
int[] pgnoOvfl = { 0 };
int nHeader;
SqlJetBtreeCellInfo info;
assert (pPage.pBt.mutex.held());
/*
* pPage is not necessarily writeable since pCell might be auxiliary*
* buffer space that is separate from the pPage buffer area
*/
assert (pCell.getBuffer() != pPage.aData.getBuffer() || pPage.pDbPage.isWriteable());
/* Fill in the header. */
nHeader = 0;
if (!pPage.leaf) {
nHeader += 4;
}
if (pPage.hasData) {
nHeader += putVarint(pointer(pCell, nHeader), nData + nZero);
} else {
nData = nZero = 0;
}
nHeader += putVarint(pointer(pCell, nHeader), nKey);
info = pPage.parseCellPtr(pCell);
assert (info.nHeader == nHeader);
assert (info.nKey == nKey);
assert (info.nData == nData + nZero);
/* Fill in the payload */
nPayload = nData + nZero;
if (pPage.intKey) {
pSrc = pData;
nSrc = nData;
nData = 0;
} else {
/* TBD: Perhaps raise SQLITE_CORRUPT if nKey is larger than 31 bits? */
nPayload += (int) nKey;
pSrc = pKey;
nSrc = (int) nKey;
}
pnSize = info.nSize;
spaceLeft = info.nLocal;
pPayload = pointer(pCell, nHeader);
pPrior = pointer(pCell, info.iOverflow);
while (nPayload > 0) {
if (spaceLeft == 0) {
int pgnoPtrmap = pgnoOvfl[0]; /*
* Overflow page pointer-map entry
* page
*/
if (pBt.autoVacuum) {
do {
pgnoOvfl[0]++;
} while (pBt.PTRMAP_ISPAGE(pgnoOvfl[0]) || pgnoOvfl[0] == pBt.PENDING_BYTE_PAGE());
}
try {
pOvfl = pBt.allocatePage(pgnoOvfl, pgnoOvfl[0], false);
/*
* If the database supports auto-vacuum, and the second or
* subsequent* overflow page is being allocated, add an
* entry to the pointer-map* for that page now.** If this is
* the first overflow page, then write a partial entry* to
* the pointer-map. If we write nothing to this pointer-map
* slot,* then the optimistic overflow chain processing in
* clearCell()* may misinterpret the uninitialised values
* and delete the* wrong pages from the database.
*/
if (pBt.autoVacuum) {
byte eType = (pgnoPtrmap != 0 ? SqlJetBtreeShared.PTRMAP_OVERFLOW2
: SqlJetBtreeShared.PTRMAP_OVERFLOW1);
try {
pBt.ptrmapPut(pgnoOvfl[0], eType, pgnoPtrmap);
} catch (SqlJetException e) {
releasePage(pOvfl);
}
}
} catch (SqlJetException e) {
releasePage(pToRelease);
throw e;
}
/*
* If pToRelease is not zero than pPrior points into the data
* area* of pToRelease. Make sure pToRelease is still writeable.
*/
assert (pToRelease == null || pToRelease.pDbPage.isWriteable());
/*
* If pPrior is part of the data area of pPage, then make sure
* pPage* is still writeable
*/
assert (pPrior.getBuffer() != pPage.aData.getBuffer() || pPage.pDbPage.isWriteable());
put4byte(pPrior, pgnoOvfl[0]);
releasePage(pToRelease);
pToRelease = pOvfl;
pPrior = pOvfl.aData;
put4byte(pPrior, 0);
pPayload = pointer(pOvfl.aData, 4);
spaceLeft = pBt.usableSize - 4;
}
n = nPayload;
if (n > spaceLeft)
n = spaceLeft;
/*
* If pToRelease is not zero than pPayload points into the data area
* * of pToRelease. Make sure pToRelease is still writeable.
*/
assert (pToRelease == null || pToRelease.pDbPage.isWriteable());
/*
* If pPayload is part of the data area of pPage, then make sure
* pPage* is still writeable
*/
assert (pPayload.getBuffer() != pPage.aData.getBuffer() || pPage.pDbPage.isWriteable());
if (nSrc > 0) {
if (n > nSrc)
n = nSrc;
assert (pSrc != null);
memcpy(pPayload, pSrc, n);
} else {
memset(pPayload, (byte) 0, n);
}
nPayload -= n;
movePtr(pPayload, n);
pSrc = pointer(pSrc, n);
nSrc -= n;
spaceLeft -= n;
if (nSrc == 0) {
nSrc = nData;
pSrc = pData;
}
}
releasePage(pToRelease);
return pnSize;
}
/**
* If the cell with index iCell on page pPage contains a pointer to an
* overflow page, insert an entry into the pointer-map for the overflow
* page.
*
* @param iCell
*
* @throws SqlJetException
*/
public void ptrmapPutOvfl(int iCell) throws SqlJetException {
SqlJetMemPage pPage = this;
ISqlJetMemoryPointer pCell;
assert (pPage.pBt.mutex.held());
pCell = pPage.findOverflowCell(iCell);
pPage.ptrmapPutOvflPtr(pCell);
}
}