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

639 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.ArrayList;
import java.util.NoSuchElementException;
public class FieldIndexTable extends IndexTable {
private static final Class<?>[] fieldClasses = {};
private static final String[] fieldNames = {};
private final Schema indexSchema;
private final int indexColumn;
/**
* Construct a new secondary index which is based upon a specific field within the
* primary table specified by name.
* @param primaryTable primary table.
* @param colIndex identifies the indexed column within the primary table.
* @throws IOException thrown if an IO error occurs
*/
FieldIndexTable(Table primaryTable, int colIndex) throws IOException {
this(primaryTable, primaryTable.getDBHandle().getMasterTable().createTableRecord(
primaryTable.getName(), getIndexTableSchema(primaryTable, colIndex), 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 an IO error occurs
*/
FieldIndexTable(Table primaryTable, TableRecord indexTableRecord) throws IOException {
super(primaryTable, indexTableRecord);
this.indexSchema = indexTable.getSchema();
this.indexColumn = indexTableRecord.getIndexedColumn();
}
private static Schema getIndexTableSchema(Table primaryTable, int colIndex) {
byte fieldType = primaryTable.getSchema().getField(colIndex).getFieldType();
IndexField indexKeyField = IndexField.getIndexField(fieldType);
return new Schema(0, indexKeyField.getClass(), "IndexKey", fieldClasses, fieldNames);
}
/*
* @see ghidra.framework.store.db.IndexTable#findPrimaryKeys(ghidra.framework.store.db.Field)
*/
@Override
long[] findPrimaryKeys(Field indexValue) throws IOException {
IndexField indexField = IndexField.getIndexField(indexValue, Long.MIN_VALUE);
DBFieldIterator iter = indexTable.fieldKeyIterator(indexField);
ArrayList<IndexField> list = new ArrayList<>(20);
while (iter.hasNext()) {
IndexField f = (IndexField) iter.next();
if (!f.hasSameIndex(indexField)) {
break;
}
if (indexField.usesTruncatedFieldValue()) {
// Must check actual record if index value was truncated
Record rec = primaryTable.getRecord(f.getPrimaryKey());
Field val = rec.getField(indexColumn);
if (!indexValue.equals(val)) {
continue;
}
}
list.add(f);
}
long[] keys = new long[list.size()];
for (int i = 0; i < keys.length; i++) {
IndexField f = list.get(i);
keys[i] = f.getPrimaryKey();
}
return keys;
}
/*
* @see ghidra.framework.store.db.IndexTable#getKeyCount(ghidra.framework.store.db.Field)
*/
@Override
int getKeyCount(Field indexValue) throws IOException {
return findPrimaryKeys(indexValue).length;
}
/*
* @see ghidra.framework.store.db.IndexTable#addEntry(ghidra.framework.store.db.Record)
*/
@Override
void addEntry(Record record) throws IOException {
Field indexedField = record.getField(colIndex);
IndexField f = IndexField.getIndexField(indexedField, record.getKey());
Record rec = indexSchema.createRecord(f);
indexTable.putRecord(rec);
}
/*
* @see ghidra.framework.store.db.IndexTable#deleteEntry(ghidra.framework.store.db.Record)
*/
@Override
void deleteEntry(Record record) throws IOException {
Field indexedField = record.getField(colIndex);
IndexField f = IndexField.getIndexField(indexedField, record.getKey());
indexTable.deleteRecord(f);
}
/*
* @see ghidra.framework.store.db.IndexTable#indexIterator()
*/
@Override
DBFieldIterator indexIterator() throws IOException {
return new IndexFieldIterator();
}
/*
* @see ghidra.framework.store.db.IndexTable#indexIterator(ghidra.framework.store.db.Field, ghidra.framework.store.db.Field, boolean)
*/
@Override
DBFieldIterator indexIterator(Field minField, Field maxField, boolean before)
throws IOException {
return new IndexFieldIterator(minField, maxField, before);
}
/*
* @see db.IndexTable#indexIterator(db.Field, db.Field, db.Field, boolean)
*/
@Override
DBFieldIterator indexIterator(Field minField, Field maxField, Field startField, boolean before)
throws IOException {
return new IndexFieldIterator(minField, maxField, startField, before);
}
/**
* Iterates over index field values within a specified range.
* NOTE: Index fields which have been truncated may be returned out of order.
*/
class IndexFieldIterator implements DBFieldIterator {
private IndexField min;
private IndexField max;
private IndexField lastKey;
private IndexField indexKey;
private DBFieldIterator indexIterator;
private boolean hasNext = false;
private boolean hasPrev = false;
/**
* Construct an index field iterator starting with the minimum index value.
*/
IndexFieldIterator() throws IOException {
this(null, null, true);
}
/**
* Construct an index field iterator. The iterator is positioned at index
* value identified by startValue.
* @param minValue minimum index value or null if no minimum
* @param maxValue maximum index value or null if no maximum
* @param before if true initial position is before minValue, else position
* after maxValue
* @throws IOException
*/
IndexFieldIterator(Field minValue, Field maxValue, boolean before) throws IOException {
if (primaryTable.getSchema().getField(indexColumn).isVariableLength()) {
throw new UnsupportedOperationException(
"Due to potential truncation issues, operation not permitted on variable length fields");
}
min = minValue != null ? IndexField.getIndexField(minValue, Long.MIN_VALUE) : null;
max = maxValue != null ? IndexField.getIndexField(maxValue, Long.MAX_VALUE) : null;
IndexField start = null;
if (before && minValue != null) {
start = min;
}
else if (!before && maxValue != null) {
start = max;
}
if (start != null) {
indexIterator = indexTable.fieldKeyIterator(min, max, start);
}
else {
indexIterator = indexTable.fieldKeyIterator(min, max, before);
}
if (indexIterator.hasNext()) {
indexIterator.next();
if (before) {
indexIterator.previous();
}
}
}
/**
* @param minField
* @param maxField
* @param startField
* @param before
* @throws IOException
*/
public IndexFieldIterator(Field minValue, Field maxValue, Field startValue, boolean before)
throws IOException {
if (primaryTable.getSchema().getField(indexColumn).isVariableLength()) {
throw new UnsupportedOperationException(
"Due to potential truncation issues, operation not permitted on variable length fields");
}
if (startValue == null) {
throw new IllegalArgumentException("starting index value required");
}
min = minValue != null ? IndexField.getIndexField(minValue, Long.MIN_VALUE) : null;
max = maxValue != null ? IndexField.getIndexField(maxValue, Long.MAX_VALUE) : null;
IndexField start =
IndexField.getIndexField(startValue, before ? Long.MIN_VALUE : Long.MAX_VALUE);
indexIterator = indexTable.fieldKeyIterator(min, max, start);
if (indexIterator.hasNext()) {
IndexField f = (IndexField) indexIterator.next();
if (before || !f.getIndexField().equals(startValue)) {
indexIterator.previous();
}
}
}
@Override
public boolean hasNext() throws IOException {
if (hasNext)
return true;
hasPrev = false; // TODO ???
indexKey = (IndexField) indexIterator.next();
int skipCnt = 0;
while (indexKey != null && indexKey.hasSameIndex(lastKey)) {
if (++skipCnt > 10) {
// Reinit iterator to skip large number of same index value
indexIterator = indexTable.fieldKeyIterator(min, max,
IndexField.getIndexField(indexKey.getIndexField(), Long.MAX_VALUE));
skipCnt = 0;
}
indexKey = (IndexField) indexIterator.next();
}
if (indexKey == null)
return false;
hasNext = true;
return true;
}
@Override
public boolean hasPrevious() throws IOException {
if (hasPrev)
return true;
hasNext = false; // TODO ???
indexKey = (IndexField) indexIterator.previous();
int skipCnt = 0;
while (indexKey != null && indexKey.hasSameIndex(lastKey)) {
if (++skipCnt > 10) {
// Reinit iterator to skip large number of same index value
indexIterator = indexTable.fieldKeyIterator(min, max,
IndexField.getIndexField(indexKey.getIndexField(), Long.MIN_VALUE));
skipCnt = 0;
}
indexKey = (IndexField) indexIterator.previous();
}
if (indexKey == null)
return false;
hasPrev = true;
return true;
}
@Override
public Field next() throws IOException {
if (hasNext || hasNext()) {
hasNext = false;
hasPrev = true;
lastKey = indexKey;
Field f = indexKey.getIndexField();
return f.newField(f);
}
return null;
}
@Override
public Field previous() throws IOException {
if (hasPrev || hasPrevious()) {
hasNext = true;
hasPrev = false;
lastKey = indexKey;
Field f = indexKey.getIndexField();
return f.newField(f);
}
return null;
}
/**
* Delete all primary records which have the current
* index value (lastKey).
* @see db.DBFieldIterator#delete()
*/
@Override
public boolean delete() throws IOException {
if (lastKey == null)
return false;
synchronized (db) {
long[] keys = findPrimaryKeys(lastKey.getIndexField());
for (int i = 0; i < keys.length; i++) {
primaryTable.deleteRecord(keys[i]);
}
lastKey = null;
return true;
}
}
}
/* (non-Javadoc)
* @see ghidra.framework.store.db.IndexTable#hasRecord(ghidra.framework.store.db.Field)
*/
@Override
boolean hasRecord(Field field) throws IOException {
IndexField indexField = IndexField.getIndexField(field, Long.MIN_VALUE);
DBFieldIterator iter = indexTable.fieldKeyIterator(indexField);
while (iter.hasNext()) {
IndexField f = (IndexField) iter.next();
if (!f.hasSameIndex(indexField)) {
return false;
}
if (indexField.usesTruncatedFieldValue()) {
// Must check actual record if index value was truncated
Record rec = primaryTable.getRecord(f.getPrimaryKey());
Field val = rec.getField(indexColumn);
if (!field.equals(val)) {
continue; // skip
}
}
return true;
}
return false;
}
/**
* Iterate over all primary keys sorted based upon the associated index key.
* @return primary key iterator
* @throws IOException thrown if IO error occurs
*/
@Override
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
*/
@Override
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
*/
@Override
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
*/
@Override
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
*/
@Override
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 startField through endField, inclusive.
* If atStart is true, the iterator is initially positioned before the first index
* buffer whose index key is greater than or equal to the specified startField value.
* If atStart is false, the iterator is initially positioned after the first index
* buffer whose index key is less than or equal to the specified endField value.
* @param startField minimum index key value
* @param endField maximum index key value
* @param atStart if true, position iterator before start value.
* Otherwise, position iterator after end value.
* @return primary key iterator
* @throws IOException thrown if IO error occurs
*/
@Override
DBLongIterator keyIterator(Field startField, Field endField, boolean atStart)
throws IOException {
return new PrimaryKeyIterator(startField, endField, atStart ? startField : endField,
atStart ? Long.MIN_VALUE : Long.MAX_VALUE, !atStart);
}
/**
* @see db.IndexTable#keyIterator(db.Field, db.Field, db.Field, boolean)
*/
@Override
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 which correspond to index field values within a specified range.
* NOTE: Primary keys corresponding to index fields which have been truncated may be returned out of order.
*/
private class PrimaryKeyIterator implements DBLongIterator {
private IndexField min;
private IndexField max;
private DBFieldIterator indexIterator;
private boolean hasNext = false;
private boolean hasPrev = false;
private IndexField key;
private IndexField lastKey;
/**
* Construct a key iterator starting with the minimum secondary key.
*/
PrimaryKeyIterator() throws IOException {
indexIterator = indexTable.fieldKeyIterator();
}
/**
* 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 (ignored if startValue is null).
* @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 {
min = minValue != null ? IndexField.getIndexField(minValue, Long.MIN_VALUE) : null;
max = maxValue != null ? IndexField.getIndexField(maxValue, Long.MAX_VALUE) : null;
IndexField start = null;
if (after && startValue == null && maxValue == null) {
indexIterator = indexTable.fieldKeyIterator(min, max, !after);
if (indexIterator.hasNext()) {
indexIterator.next(); // must step beyond max indexed value
}
}
else {
start =
startValue != null ? IndexField.getIndexField(startValue, primaryKey) : null;
indexIterator = indexTable.fieldKeyIterator(min, max, start);
if (indexIterator.hasNext()) {
Field f = indexIterator.next();
if (!after || !f.equals(start)) {
indexIterator.previous();
}
}
}
}
/**
* If min or max index values was truncated, a comparison of the actual
* indexed field value (i.e., primary table value) is done with the min and/or max values.
* @param f index field from index table iterator
* @return true if field value corresponding to f is outside the min/max range.
* It is assumed that the underlying table iterator will not return index values
* out of range which do not have the same truncated index value.
* @throws IOException
*/
private boolean indexValueOutOfRange(IndexField f) throws IOException {
Field val = null;
if (min != null && min.usesTruncatedFieldValue() && min.hasSameIndex(f)) {
Record rec = primaryTable.getRecord(f.getPrimaryKey());
val = rec.getField(indexColumn);
if (val.compareTo(min.getNonTruncatedIndexField()) < 0) {
return true;
}
}
if (max != null && max.usesTruncatedFieldValue() && max.hasSameIndex(f)) {
if (val == null) {
Record rec = primaryTable.getRecord(f.getPrimaryKey());
val = rec.getField(indexColumn);
}
if (val.compareTo(min.getNonTruncatedIndexField()) > 0) {
return true;
}
}
return false;
}
/* (non-Javadoc)
* @see ghidra.framework.store.db.DBLongIterator#hasNext()
*/
@Override
public boolean hasNext() throws IOException {
if (hasNext) {
return true;
}
while (indexIterator.hasNext()) {
hasPrev = false;
key = (IndexField) indexIterator.next();
if (!indexValueOutOfRange(key)) {
hasNext = true;
break;
}
}
return hasNext;
}
/* (non-Javadoc)
* @see ghidra.framework.store.db.DBLongIterator#hasPrevious()
*/
@Override
public boolean hasPrevious() throws IOException {
if (hasPrev) {
return true;
}
while (indexIterator.hasPrevious()) {
hasNext = false;
key = (IndexField) indexIterator.previous();
if (!indexValueOutOfRange(key)) {
hasPrev = true;
break;
}
}
return hasPrev;
}
/* (non-Javadoc)
* @see ghidra.framework.store.db.DBLongIterator#next()
*/
@Override
public long next() throws IOException {
if (hasNext()) {
lastKey = key;
hasNext = false;
return key.getPrimaryKey();
}
throw new NoSuchElementException();
}
/* (non-Javadoc)
* @see ghidra.framework.store.db.DBLongIterator#previous()
*/
@Override
public long previous() throws IOException {
if (hasPrevious()) {
lastKey = key;
hasPrev = false;
return key.getPrimaryKey();
}
throw new NoSuchElementException();
}
/* (non-Javadoc)
* @see ghidra.framework.store.db.DBLongIterator#delete()
*/
@Override
public boolean delete() throws IOException {
if (lastKey != null) {
long primaryKey = lastKey.getPrimaryKey();
lastKey = null;
return primaryTable.deleteRecord(primaryKey);
}
return false;
}
}
}