ghidra/Ghidra/Framework/DB/src/main/java/db/IndexTable.java

627 lines
20 KiB
Java

/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package db;
import java.io.IOException;
import java.util.NoSuchElementException;
import ghidra.util.exception.AssertException;
import ghidra.util.exception.CancelledException;
import ghidra.util.task.TaskMonitor;
/**
* <code>IndexTable</code> maintains a secondary index within a private Table instance.
* This index facilitates the indexing of non-unique secondary keys within a
* user Table.
*/
abstract class IndexTable {
protected static final long[] emptyKeyArray = new long[0];
/**
* Database Handle
*/
protected final DBHandle db;
/**
* Master table record for this index table.
*/
protected final TableRecord indexTableRecord;
/**
* Primary table
*/
protected Table primaryTable;
/**
* Underlying table which contains secondary index data.
*/
protected Table indexTable;
/**
* Field type associated with indexed column.
*/
protected final Field fieldType;
/**
* Indexed column within primary table schema.
*/
protected final int colIndex;
/**
* Construct a new or existing secondary index. An existing index must have
* its' root ID specified within the tableRecord.
* @param primaryTable primary table.
* @param indexTableRecord specifies the index parameters.
* @throws IOException thrown if IO error occurs
*/
IndexTable(Table primaryTable, TableRecord indexTableRecord) throws IOException {
if (!primaryTable.useLongKeys())
throw new AssertException("Only long-key tables may be indexed");
this.db = primaryTable.getDBHandle();
this.primaryTable = primaryTable;
this.indexTableRecord = indexTableRecord;
this.indexTable = new Table(primaryTable.getDBHandle(), indexTableRecord);
this.colIndex = indexTableRecord.getIndexedColumn();
fieldType = primaryTable.getSchema().getField(indexTableRecord.getIndexedColumn());
primaryTable.addIndex(this);
}
/**
* Get the index table for a specified index TableRecord.
* @param db database handle
* @param indexTableRecord master table record for an index table
* @return IndexTable index table
* @throws IOException thrown if IO error occurs
*/
static IndexTable getIndexTable(DBHandle db, TableRecord indexTableRecord) throws IOException {
String name = indexTableRecord.getName();
Table primaryTable = db.getTable(name);
if (primaryTable == null)
throw new AssertException("Table not found: " + name);
if (indexTableRecord.getSchema().getKeyFieldType() instanceof IndexField) {
return new FieldIndexTable(primaryTable, indexTableRecord);
}
Field fieldType = primaryTable.getSchema().getField(indexTableRecord.getIndexedColumn());
if (fieldType.isVariableLength()) {
return new VarIndexTable(primaryTable, indexTableRecord);
}
return new FixedIndexTable(primaryTable, indexTableRecord);
}
/**
* Create a new index table for an empty primary table.
* @param primaryTable primary table to be indexed
* @param indexColumn primary table column to be indexed
* @return IndexTable new index table
* @throws IOException thrown if IO error occurs
*/
static IndexTable createIndexTable(Table primaryTable, int indexColumn) throws IOException {
if (primaryTable.getRecordCount() != 0)
throw new AssertException();
return new FieldIndexTable(primaryTable, indexColumn);
}
/**
* Check the consistency of this index table.
* @return true if consistency check passed, else false
* @throws IOException
* @throws CancelledException
*/
boolean isConsistent(TaskMonitor monitor) throws IOException, CancelledException {
return indexTable.isConsistent(primaryTable.getSchema().getFieldNames()[colIndex], monitor);
}
/**
* Get the table number associated with the underlying index table.
* @return table number
*/
long getTableNum() {
return indexTable.getTableNum();
}
/**
* Get the indexed column within the primary table schema.
* @return indexed column number
*/
int getColumnIndex() {
return colIndex;
}
/**
* Get index table statistics
* @return statistics data
* @throws IOException thrown if IO error occurs
*/
TableStatistics getStatistics() throws IOException {
TableStatistics stats = indexTable.getStatistics();
stats.indexColumn = colIndex;
return stats;
}
/**
* Determine if there is an occurance of the specified index key value.
* @param field index key value
* @return true if an index key value equal to field exists.
*/
boolean hasRecord(Field field) throws IOException {
return indexTable.hasRecord(field);
}
/**
* Find all primary keys which correspond to the specified indexed field
* value.
* @param field the field value to search for.
* @return list of primary keys
*/
abstract long[] findPrimaryKeys(Field indexValue) throws IOException;
/**
* Get the number of primary keys which correspond to the specified indexed field
* value.
* @param field the field value to search for.
* @return key count
*/
abstract int getKeyCount(Field indexValue) throws IOException;
/**
* Add an entry to this index. Caller is responsible for ensuring that this
* is not a duplicate entry.
* @param record new record
* @throws IOException
*/
abstract void addEntry(Record record) throws IOException;
/**
* Delete an entry from this index.
* @param record deleted record
* @throws IOException
*/
abstract void deleteEntry(Record record) throws IOException;
/**
* Delete all records within this index table.
*/
void deleteAll() throws IOException {
indexTable.deleteAll();
}
/**
* Iterate over all index keys. Index keys are sorted in ascending order.
* @return index key iterator
* @throws IOException thrown if IO error occurs
*/
abstract DBFieldIterator indexIterator() throws IOException;
/**
* Iterate over all the unique index field values within the specified range identified
* by minField and maxField. Index values are returned in an ascending sorted order.
* @param minField minimum index column value, if null absolute minimum is used
* @param maxField maximum index column value, if null absolute maximum is used
* @param before if true initial position is before minField, else position
* is after endField
* @return index field iterator.
* @throws IOException
*/
abstract DBFieldIterator indexIterator(Field minField, Field maxField, boolean before)
throws IOException;
/**
* Iterate over all the unique index field values within the specified range identified
* by minField and maxField. Index values are returned in an ascending sorted order with the
* initial iterator position corresponding to the startField.
* @param minField minimum index column value, if null absolute minimum is used
* @param maxField maximum index column value, if null absolute maximum is used
* @param startField index column value corresponding to initial position of iterator
* @param before if true initial position is before startField value, else position
* is after startField value
* @return index field iterator.
* @throws IOException
*/
abstract DBFieldIterator indexIterator(Field minField, Field maxField, Field startField,
boolean before) throws IOException;
/**
* Iterate over all primary keys sorted based upon the associated index key.
* @return primary key iterator
* @throws IOException thrown if IO error occurs
*/
DBLongIterator keyIterator() throws IOException {
return new PrimaryKeyIterator();
}
/**
* Iterate over all primary keys sorted based upon the associated index key.
* The iterator is initially positioned before the first index buffer whose index key
* is greater than or equal to the specified startField value.
* @param startField index key value which determines initial position of iterator
* @return primary key iterator
* @throws IOException thrown if IO error occurs
*/
DBLongIterator keyIteratorBefore(Field startField) throws IOException {
return new PrimaryKeyIterator(startField, false);
}
/**
* Iterate over all primary keys sorted based upon the associated index key.
* The iterator is initially positioned after the index buffer whose index key
* is equal to the specified startField value or immediately before the first
* index buffer whose index key is greater than the specified startField value.
* @param startField index key value which determines initial position of iterator
* @return primary key iterator
* @throws IOException thrown if IO error occurs
*/
DBLongIterator keyIteratorAfter(Field startField) throws IOException {
return new PrimaryKeyIterator(startField, true);
}
/**
* Iterate over all primary keys sorted based upon the associated index key.
* The iterator is initially positioned before the primaryKey within the index buffer
* whose index key is equal to the specified startField value or immediately before the first
* index buffer whose index key is greater than the specified startField value.
* @param startField index key value which determines initial position of iterator
* @param primaryKey initial position within index buffer if index key matches startField value.
* @return primary key iterator
* @throws IOException thrown if IO error occurs
*/
DBLongIterator keyIteratorBefore(Field startField, long primaryKey) throws IOException {
return new PrimaryKeyIterator(null, null, startField, primaryKey, false);
}
/**
* Iterate over all primary keys sorted based upon the associated index key.
* The iterator is initially positioned after the primaryKey within the index buffer
* whose index key is equal to the specified startField value or immediately before the first
* index buffer whose index key is greater than the specified startField value.
* @param startField index key value which determines initial position of iterator
* @param primaryKey initial position within index buffer if index key matches startField value.
* @return primary key iterator
* @throws IOException thrown if IO error occurs
*/
DBLongIterator keyIteratorAfter(Field startField, long primaryKey) throws IOException {
return new PrimaryKeyIterator(null, null, startField, primaryKey, true);
}
/**
* Iterate over all primary keys sorted based upon the associated index key.
* The iterator is limited to range of index keys of minField through maxField, inclusive.
* If before is true, the iterator is initially positioned before the first index
* buffer whose index key is greater than or equal to the specified minField value.
* If before is false, the iterator is initially positioned after the first index
* buffer whose index key is less than or equal to the specified maxField value.
* @param minField minimum index key value
* @param maxField maximum index key value
* @param before if true, position iterator before minField value,
* Otherwise, position iterator after maxField value.
* @return primary key iterator
* @throws IOException thrown if IO error occurs
*/
DBLongIterator keyIterator(Field minField, Field maxField, boolean before) throws IOException {
Field startField = before ? minField : maxField;
if (startField == null && !before) {
}
return new PrimaryKeyIterator(minField, maxField, before ? minField : maxField,
before ? Long.MIN_VALUE : Long.MAX_VALUE, !before);
}
/**
* Iterate over all primary keys sorted based upon the associated index key.
* The iterator is limited to range of index keys of minField through maxField, inclusive.
* The iterator is initially positioned before or after the specified startField index value.
* @param minField minimum index key value
* @param maxField maximum index key value
* @param startField starting indexed value position
* @param before if true positioned before startField value, else positioned after maxField value
* @return primary key iterator
* @throws IOException thrown if IO error occurs
*/
DBLongIterator keyIterator(Field minField, Field maxField, Field startField, boolean before)
throws IOException {
return new PrimaryKeyIterator(minField, maxField, startField,
before ? Long.MIN_VALUE : Long.MAX_VALUE, !before);
}
/**
* Iterates over primary keys for a range of index field values.
*/
private class PrimaryKeyIterator implements DBLongIterator {
private RecordIterator indexIterator;
private int expectedModCount;
private int index;
private long indexPrimaryKey;
private IndexBuffer indexBuffer;
private boolean forward = true;
private boolean reverse = true;
private Field lastKey;
private boolean hasPrev = false;
private boolean hasNext = false;
/**
* Construct a key iterator starting with the minimum secondary key.
*/
PrimaryKeyIterator() throws IOException {
expectedModCount = indexTable.modCount;
indexIterator = indexTable.iterator();
}
/**
* Construct a key iterator. The iterator is positioned immediately before
* the key associated with the first occurance of the startValue.
* @param startValue indexed field value.
* @param after if true the iterator is positioned immediately after
* the last occurance of the specified startValue position.
*/
PrimaryKeyIterator(Field startValue, boolean after) throws IOException {
this(null, null, startValue, after ? Long.MAX_VALUE : Long.MIN_VALUE, after);
}
/**
* Construct a key iterator. The iterator is positioned immediately before
* or after the key associated with the specified startValue/primaryKey.
* @param minValue minimum index value or null if no minimum
* @param maxValue maximum index value or null if no maximum
* @param startValue starting index value.
* @param primaryKey starting primary key value.
* @param after if true iterator is positioned immediately after
* the startValue/primaryKey,
* otherwise immediately before.
* @throws IOException
*/
PrimaryKeyIterator(Field minValue, Field maxValue, Field startValue, long primaryKey,
boolean after) throws IOException {
expectedModCount = indexTable.modCount;
indexIterator = indexTable.iterator(minValue, maxValue, startValue);
if (hasNext()) {
if (startValue.equals(indexBuffer.getIndexKey())) {
index = indexBuffer.getIndex(primaryKey);
if (index < 0) {
index = -index - 1;
}
else if (after) {
index++;
}
if (index == indexBuffer.keyCount) {
--index;
indexPrimaryKey = indexBuffer.getPrimaryKey(index);
hasNext = false;
hasPrev = true;
}
}
}
}
@Override
public boolean hasNext() throws IOException {
if (hasNext) {
return true;
}
synchronized (db) {
// Handle concurrent modification if necessary
// This is a slightly lazy approach which could miss keys added to the end of an index buffer
if (indexBuffer != null && index < (indexBuffer.keyCount - 1) &&
indexTable.modCount != expectedModCount) {
// refetch index buffer which may have changed
Field indexKey = indexBuffer.getIndexKey();
Record indexRecord;
if (indexKey.isVariableLength()) {
indexRecord = indexTable.getRecord(indexKey);
}
else {
indexRecord = indexTable.getRecord(indexKey.getLongValue());
}
if (indexRecord != null) {
// recover position within index buffer
indexBuffer = new IndexBuffer(indexKey, indexRecord.getBinaryData(0));
index = indexBuffer.getIndex(indexPrimaryKey + 1);
if (index < 0) {
index = -index - 1;
if (index == indexBuffer.keyCount) {
// next must be found in next index buffer below
indexBuffer = null;
}
else {
indexPrimaryKey = indexBuffer.getPrimaryKey(index);
hasNext = true;
}
}
else {
indexPrimaryKey = indexBuffer.getPrimaryKey(index);
hasNext = true;
}
}
else {
// index buffer no longer exists - will need to get next buffer below
indexBuffer = null;
}
hasPrev = false;
}
if (!hasNext) {
// Goto next index buffer
if ((indexBuffer == null || index >= (indexBuffer.keyCount) - 1)) {
// get next index buffer
Record indexRecord = indexIterator.next();
if (indexRecord != null) {
if (!forward) {
indexRecord = indexIterator.next();
forward = true;
}
reverse = false;
if (indexRecord != null) {
indexBuffer =
new IndexBuffer(fieldType.newField(indexRecord.getKeyField()),
indexRecord.getBinaryData(0));
index = 0;
indexPrimaryKey = indexBuffer.getPrimaryKey(index);
hasNext = true;
hasPrev = false;
}
}
}
// Step within current index buffer
else {
++index;
indexPrimaryKey = indexBuffer.getPrimaryKey(index);
hasNext = true;
hasPrev = false;
}
}
expectedModCount = indexTable.modCount;
return hasNext;
}
}
@Override
public boolean hasPrevious() throws IOException {
if (hasPrev) {
return true;
}
synchronized (db) {
// Handle concurrent modification if necessary
// This is a slightly lazy approach which could miss keys added to the beginning of an index buffer
if (indexBuffer != null && index > 0 && indexTable.modCount != expectedModCount) {
// refetch index buffer which may have changed
Field indexKey = indexBuffer.getIndexKey();
Record indexRecord; // refetch index buffer which may have changed
if (indexKey.isVariableLength()) {
indexRecord = indexTable.getRecord(indexKey);
}
else {
indexRecord = indexTable.getRecord(indexKey.getLongValue());
}
if (indexRecord != null) {
// recover position within index buffer
indexBuffer = new IndexBuffer(indexKey, indexRecord.getBinaryData(0));
index = indexBuffer.getIndex(indexPrimaryKey - 1);
if (index < 0) {
index = -index - 1;
if (index == 0) {
// previous must be found in previous index buffer below
indexBuffer = null;
}
else {
--index;
indexPrimaryKey = indexBuffer.getPrimaryKey(index);
hasPrev = true;
}
}
else {
indexPrimaryKey = indexBuffer.getPrimaryKey(index);
hasPrev = true;
}
}
else {
indexBuffer = null;
}
hasNext = false;
}
if (!hasPrev) {
// Goto previous index buffer
if ((indexBuffer == null || index == 0)) {
// get previous index buffer
Record indexRecord = indexIterator.previous();
if (indexRecord != null) {
if (!reverse) {
indexRecord = indexIterator.previous();
reverse = true;
}
forward = false;
if (indexRecord != null) {
indexBuffer =
new IndexBuffer(fieldType.newField(indexRecord.getKeyField()),
indexRecord.getBinaryData(0));
index = indexBuffer.keyCount - 1;
indexPrimaryKey = indexBuffer.getPrimaryKey(index);
hasNext = false;
hasPrev = true;
}
}
}
// Step within current index buffer
else {
--index;
indexPrimaryKey = indexBuffer.getPrimaryKey(index);
hasNext = false;
hasPrev = true;
}
}
expectedModCount = indexTable.modCount;
return hasPrev;
}
}
@Override
public long next() throws IOException {
if (hasNext || hasNext()) {
long key = indexBuffer.getPrimaryKey(index);
lastKey = new LongField(key);
hasNext = false;
hasPrev = true;
return key;
}
throw new NoSuchElementException();
}
@Override
public long previous() throws IOException {
if (hasPrev || hasPrevious()) {
long key = indexBuffer.getPrimaryKey(index);
lastKey = new LongField(key);
hasNext = true;
hasPrev = false;
return key;
}
throw new NoSuchElementException();
}
/**
* WARNING: This could be slow since the index buffer must be read
* after each record deletion.
* @see db.DBLongIterator#delete()
*/
@Override
public boolean delete() throws IOException {
if (lastKey == null)
return false;
synchronized (db) {
long key = lastKey.getLongValue();
primaryTable.deleteRecord(key);
lastKey = null;
return true;
}
}
}
}