Merge remote-tracking branch 'origin/GT-2824-dragonmacher'

Conflicts:
	Ghidra/Features/Base/src/main/java/ghidra/app/script/ScriptInfo.java
This commit is contained in:
ghidravore 2019-05-02 16:32:18 -04:00
commit 041e59aeaf
24 changed files with 1503 additions and 672 deletions

View file

@ -3365,12 +3365,15 @@ public class CodeManager implements ErrorHandler, ManagerDB {
if (newComment == null) {
newComment = "";
}
StringDiff[] diffs = getLineDiffs(newComment, oldComment);
StringDiff[] diffs = StringDiffUtils.getLineDiffs(newComment, oldComment);
long date = System.currentTimeMillis();
long addr = addrMap.getKey(address, true);
try {
for (StringDiff diff : diffs) {
historyAdapter.createRecord(addr, (byte) commentType, diff.pos1, diff.pos2,
diff.insertData);
historyAdapter.createRecord(addr, (byte) commentType, diff.start, diff.end,
diff.text, date);
}
}
catch (IOException e) {
@ -3379,7 +3382,8 @@ public class CodeManager implements ErrorHandler, ManagerDB {
}
/**
* Get the comment history for the comment type at the given address.
* Get the comment history for the comment type at the given address
*
* @param addr address for the comment history
* @param commentType comment type
* @return zero length array if no history exists
@ -3387,47 +3391,37 @@ public class CodeManager implements ErrorHandler, ManagerDB {
public CommentHistory[] getCommentHistory(Address addr, int commentType) {
lock.acquire();
try {
RecordIterator iter = historyAdapter.getRecordsByAddress(addr);
List<Record> list = new ArrayList<>();
while (iter.hasNext()) {
Record rec = iter.next();
// records are sorted by date ascending
List<Record> allRecords = getHistoryRecords(addr, commentType);
if (rec.getByteValue(CommentHistoryAdapter.HISTORY_TYPE_COL) == commentType) {
list.add(rec);
}
}
List<CommentHistory> historyList = new ArrayList<>(); // CommentHistory objects
String comments = getComments(addr, commentType);
while (list.size() > 0) {
Record rec = list.get(list.size() - 1);
List<CommentHistory> results = new ArrayList<>();
String comment = getComment(addr, commentType);
while (!allRecords.isEmpty()) {
Record rec = allRecords.get(allRecords.size() - 1);
long date = rec.getLongValue(CommentHistoryAdapter.HISTORY_DATE_COL);
List<Record> subList = findHistoryRecords(date, list);
StringDiff[] diffs = new StringDiff[subList.size()];
List<Record> records = subListByDate(allRecords, date);
String userName = null;
Date modDate = null;
for (int j = 0; j < subList.size(); j++) {
Record r = subList.get(j);
userName = r.getString(CommentHistoryAdapter.HISTORY_USER_COL);
modDate = new Date(r.getLongValue(CommentHistoryAdapter.HISTORY_DATE_COL));
List<StringDiff> diffs = new ArrayList<>(records.size());
diffs[j] = new StringDiff(r.getIntValue(CommentHistoryAdapter.HISTORY_POS1_COL),
r.getIntValue(CommentHistoryAdapter.HISTORY_POS2_COL),
r.getString(CommentHistoryAdapter.HISTORY_STRING_COL));
String user = null;
for (Record r : records) {
user = r.getString(CommentHistoryAdapter.HISTORY_USER_COL);
int pos1 = r.getIntValue(CommentHistoryAdapter.HISTORY_POS1_COL);
int pos2 = r.getIntValue(CommentHistoryAdapter.HISTORY_POS2_COL);
String data = r.getString(CommentHistoryAdapter.HISTORY_STRING_COL);
diffs.add(StringDiff.restore(data, pos1, pos2));
}
if (comments == null) {
comments = "";
}
historyList.add(new CommentHistory(addr, commentType, userName, comments, modDate));
comments = applyDiffs(comments, diffs);
int from = list.size() - subList.size();
// remove the subList elements from the list
list.subList(from, list.size()).clear();
results.add(new CommentHistory(addr, commentType, user, comment, new Date(date)));
comment = StringDiffUtils.applyDiffs(comment, diffs);
records.clear(); // remove the subList elements from the list
}
CommentHistory[] h = new CommentHistory[historyList.size()];
return historyList.toArray(h);
CommentHistory[] h = new CommentHistory[results.size()];
return results.toArray(h);
}
catch (IOException e) {
dbError(e);
@ -3438,23 +3432,39 @@ public class CodeManager implements ErrorHandler, ManagerDB {
return new CommentHistory[0];
}
private List<Record> findHistoryRecords(long date, List<Record> recList) {
int i;
for (i = recList.size() - 1; i >= 0; i--) {
Record rec = recList.get(i);
if (date != rec.getLongValue(CommentHistoryAdapter.HISTORY_DATE_COL)) {
break;
// note: you must have the lock when calling this method
private List<Record> getHistoryRecords(Address addr, int commentType) throws IOException {
RecordIterator it = historyAdapter.getRecordsByAddress(addr);
List<Record> list = new ArrayList<>();
while (it.hasNext()) {
Record rec = it.next();
if (rec.getByteValue(CommentHistoryAdapter.HISTORY_TYPE_COL) == commentType) {
list.add(rec);
}
}
return recList.subList(i + 1, recList.size());
return list;
}
private String getComments(Address addr, int commentType) throws IOException {
// note: records are sorted by date; assume that the date we seek is at the end
private List<Record> subListByDate(List<Record> records, long date) {
for (int i = records.size() - 1; i >= 0; i--) {
Record rec = records.get(i);
if (date != rec.getLongValue(CommentHistoryAdapter.HISTORY_DATE_COL)) {
return records.subList(i + 1, records.size());
}
}
// all records have the same date
return records.subList(0, records.size());
}
private String getComment(Address addr, int commentType) throws IOException {
Record record = commentAdapter.getRecord(addrMap.getKey(addr, false));
if (record != null) {
return record.getString(commentType);
}
return null;
return "";
}
public void replaceDataTypes(long oldDataTypeID, long newDataTypeID) {
@ -3631,181 +3641,4 @@ public class CodeManager implements ErrorHandler, ManagerDB {
return protoMgr.getPrototype(protoID);
}
/**
* Returns the list of StringDiff objects that if applied to s1 would result in s2; The
* given text will look only for whole lines using '\n'.
*
* @param s1 the original string
* @param s2 the result string
* this value, then a completely different string will be returned
* @return an array of StringDiff objects that change s1 into s2;
*/
private static StringDiff[] getLineDiffs(String s1, String s2) {
/**
* Minimum size used to determine whether a new StringDiff object will be
* created just using a string (no positions)
* in the <code>getDiffs(String, String)</code> method.
* @see #getLineDiffs(String, String)
*/
int MINIMUM_DIFF_SIZE = 100;
return getLineDiffs(s1, s2, MINIMUM_DIFF_SIZE);
}
/**
* Returns the list of StringDiff objects that if applied to s1 would result in s2; The
* given text will look only for whole lines using '\n'.
*
* @param s1 the original string
* @param s2 the result string
* @param minimumDiffSize the minimum length of s2 required for a diff; if s2 is less than
* this value, then a completely different string will be returned
* @return an array of StringDiff objects that change s1 into s2;
*/
private static StringDiff[] getLineDiffs(String s1, String s2, int minimumDiffSize) {
if (s2.length() < minimumDiffSize) {
return new StringDiff[] { new StringDiff(s2) };
}
List<StringDiff> list = new LinkedList<>();
int pos1 = 0;
int pos2 = 0;
int len1 = s1.length();
int len2 = s2.length();
int origPos;
while (pos1 < len1 || pos2 < len2) {
String line1 = getLine(s1, pos1);
String line2 = getLine(s2, pos2);
if (line1.equals(line2)) {
pos1 += line1.length();
pos2 += line2.length();
continue;
}
int posInOther1 = findLine(s2, pos2, line1);
origPos = pos1;
while (posInOther1 < 0) {
pos1 += line1.length();
line1 = getLine(s1, pos1);
posInOther1 = findLine(s2, pos2, line1);
}
if (pos1 > origPos) {
list.add(new StringDiff(origPos, pos1));
}
int posInOther2 = findLine(s1, pos1, line2);
origPos = pos2;
while (posInOther2 < 0) {
pos2 += line2.length();
line2 = getLine(s2, pos2);
posInOther2 = findLine(s1, pos1, line2);
}
if (pos2 > origPos) {
list.add(new StringDiff(pos1, s2.substring(origPos, pos2)));
continue;
}
int advance1 = posInOther2 - pos1;
int advance2 = posInOther1 - pos2;
if (advance1 > advance2) {
list.add(new StringDiff(pos1, s2.substring(pos2, posInOther1)));
pos2 = posInOther1;
}
else if (advance2 > advance1) {
list.add(new StringDiff(pos1, posInOther2));
pos1 = posInOther2;
}
}
return list.toArray(new StringDiff[list.size()]);
}
/**
* Finds a position in s that contains the string line. The matching string in
* s must be a "complete" line, in other words if pos > 0 then s.charAt(index-1) must be
* a newLine character and s.charAt(index+line.length()) must be a newLine or the end of
* the string.
* @param s the string to scan
* @param pos the position to begin the scan.
* @param line the line to scan for
* @return the position in s containing the line string.
*/
private static int findLine(String s, int pos, String line) {
if (line.length() == 0) {
return pos;
}
while (true) {
int index = s.indexOf(line, pos);
if (index < 0) {
return index;
}
if (index > 0 && s.charAt(index - 1) != '\n') {
pos = index + line.length();
continue;
}
if (line.endsWith("\n")) {
return index;
}
if (index + line.length() == s.length()) {
return index;
}
pos = index + line.length();
}
}
/**
* Returns a substring of s beginning at start and ending at either the end of the string or
* the first newLine at or after start.
* @param s the string to scan
* @param start the starting position for the scan
* @return A string that represents a line within s.
*/
public static String getLine(String s, int start) {
int n = s.length();
if (start >= n) {
return "";
}
int pos = start;
while (pos < n && s.charAt(pos) != '\n') {
pos++;
}
if (pos < n) {
pos++;
}
return s.substring(start, pos);
}
/**
* Applies the array of StringObjects to the string s to produce a new string. Warning - the
* diff objects cannot be applied to an arbitrary string, the String s must be the original
* String used to compute the diffs.
* @param s the original string
* @param diffs the array of StringDiff object to apply
* @return a new String resulting from applying the diffs to s.
*/
private static String applyDiffs(String s, StringDiff[] diffs) {
if (diffs.length == 0) {
return s;
}
if (diffs[0].pos1 < 0) {
return diffs[0].insertData;
}
StringBuffer buf = new StringBuffer(s.length());
int pos = 0;
for (StringDiff element : diffs) {
if (element.pos1 > pos) {
buf.append(s.substring(pos, element.pos1));
pos = element.pos1;
}
if (element.insertData != null) {
buf.append(element.insertData);
}
else {
pos = element.pos2;
}
}
if (pos < s.length()) {
buf.append(s.substring(pos));
}
return buf.toString();
}
}

View file

@ -20,6 +20,8 @@ import java.math.BigInteger;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import org.apache.commons.lang3.StringUtils;
import db.Record;
import ghidra.program.database.*;
import ghidra.program.model.address.Address;
@ -512,7 +514,7 @@ abstract class CodeUnitDB extends DatabaseObject implements CodeUnit, ProcessorC
@Override
public void setCommentAsArray(int commentType, String[] comment) {
setComment(commentType, StringUtilities.convertStringArray(comment));
setComment(commentType, StringUtils.join(comment, '\n'));
}
@Override

View file

@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,16 +15,15 @@
*/
package ghidra.program.database.code;
import java.io.IOException;
import db.*;
import ghidra.program.database.map.AddressMap;
import ghidra.program.model.address.Address;
import ghidra.util.exception.CancelledException;
import ghidra.util.exception.VersionException;
import ghidra.util.task.TaskMonitor;
import java.io.IOException;
import db.*;
/**
* Adapter for accessing records in the CommentHistory table.
*/
@ -33,10 +31,10 @@ abstract class CommentHistoryAdapter {
static final String COMMENT_HISTORY_TABLE_NAME = "Comment History";
static final Schema COMMENT_HISTORY_SCHEMA = new Schema(0, "Key", new Class[] {
LongField.class, ByteField.class, IntField.class, IntField.class, StringField.class,
StringField.class, LongField.class }, new String[] { "Address", "Comment Type", "Pos1",
"Pos2", "String Data", "User", "Date" });
static final Schema COMMENT_HISTORY_SCHEMA = new Schema(0, "Key",
new Class[] { LongField.class, ByteField.class, IntField.class, IntField.class,
StringField.class, StringField.class, LongField.class },
new String[] { "Address", "Comment Type", "Pos1", "Pos2", "String Data", "User", "Date" });
static final int HISTORY_ADDRESS_COL = 0;
static final int HISTORY_TYPE_COL = 1;
@ -79,14 +77,15 @@ abstract class CommentHistoryAdapter {
return new CommentHistoryAdapterV0(handle, addrMap.getOldAddressMap(), false);
}
catch (VersionException e) {
// use the 'no table' below
}
return new CommentHistoryAdapterNoTable();
}
private static CommentHistoryAdapter upgrade(DBHandle dbHandle, AddressMap addrMap,
CommentHistoryAdapter oldAdapter, TaskMonitor monitor) throws VersionException,
IOException, CancelledException {
CommentHistoryAdapter oldAdapter, TaskMonitor monitor)
throws VersionException, IOException, CancelledException {
AddressMap oldAddrMap = addrMap.getOldAddressMap();
@ -128,7 +127,8 @@ abstract class CommentHistoryAdapter {
}
/**
* Returns record count
* Returns the record count
* @return the record count
*/
abstract int getRecordCount();
@ -139,15 +139,16 @@ abstract class CommentHistoryAdapter {
* @param pos1 position 1 of change
* @param pos2 position 2 of change
* @param data string from the comment change
* @throws IOException
* @param date the date of the history entry
* @throws IOException if there was a problem accessing the database
*/
abstract void createRecord(long addr, byte commentType, int pos1, int pos2, String data)
throws IOException;
abstract void createRecord(long addr, byte commentType, int pos1, int pos2, String data,
long date) throws IOException;
/**
* Update record
* @param rec
* @throws IOException
* @param rec the record to update
* @throws IOException if there was a problem accessing the database
*/
abstract void updateRecord(Record rec) throws IOException;
@ -162,12 +163,15 @@ abstract class CommentHistoryAdapter {
/**
* Get an iterator over records with the given address.
* @param addr the address for which to get records
* @return the iterator
* @throws IOException if there was a problem accessing the database
*/
abstract RecordIterator getRecordsByAddress(Address addr) throws IOException;
/**
* Get an iterator over all records.
* Get an iterator over all records
* @return the iterator
* @throws IOException if there was a problem accessing the database
*/
abstract RecordIterator getAllRecords() throws IOException;

View file

@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,13 +15,12 @@
*/
package ghidra.program.database.code;
import ghidra.program.database.util.EmptyRecordIterator;
import ghidra.program.model.address.Address;
import java.io.IOException;
import db.Record;
import db.RecordIterator;
import ghidra.program.database.util.EmptyRecordIterator;
import ghidra.program.model.address.Address;
/**
* Adapter needed for a read-only version of Program that is not going
@ -30,53 +28,34 @@ import db.RecordIterator;
*/
class CommentHistoryAdapterNoTable extends CommentHistoryAdapter {
/* (non Javadoc)
* @see ghidra.program.database.code.CommentHistoryAdapter#createRecord(long, byte, int, int, java.lang.String)
*/
@Override
public void createRecord(long addr, byte commentType, int pos1, int pos2, String data)
throws IOException {
public void createRecord(long addr, byte commentType, int pos1, int pos2, String data,
long date) throws IOException {
throw new UnsupportedOperationException();
}
/**
* @see ghidra.program.database.code.CommentHistoryAdapter#getRecordsByAddress(long)
*/
@Override
public RecordIterator getRecordsByAddress(Address addr) throws IOException {
return new EmptyRecordIterator();
}
/**
* @see ghidra.program.database.code.CommentHistoryAdapter#getAllRecords()
*/
@Override
public RecordIterator getAllRecords() throws IOException {
return new EmptyRecordIterator();
}
/**
* @see ghidra.program.database.code.CommentHistoryAdapter#updateRecord(db.Record)
*/
@Override
void updateRecord(Record rec) throws IOException {
throw new UnsupportedOperationException();
}
/**
* @see ghidra.program.database.code.CommentHistoryAdapter#deleteRecords(ghidra.program.model.address.Address, ghidra.program.model.address.Address)
*/
@Override
boolean deleteRecords(Address start, Address end) throws IOException {
throw new UnsupportedOperationException();
}
/**
* @see ghidra.program.database.code.CommentHistoryAdapter#getRecordCount()
*/
@Override
int getRecordCount() {
return 0;
}
}

View file

@ -19,13 +19,12 @@ import java.io.IOException;
import db.*;
import ghidra.program.database.map.*;
import ghidra.program.database.util.DatabaseVersionException;
import ghidra.program.model.address.Address;
import ghidra.util.SystemUtilities;
import ghidra.util.exception.VersionException;
/**
* Adapter for Version 0 of the Comment History table.
* Adapter for Version 0 of the Comment History table
*/
class CommentHistoryAdapterV0 extends CommentHistoryAdapter {
@ -36,16 +35,17 @@ class CommentHistoryAdapterV0 extends CommentHistoryAdapter {
/**
* Construct a new Version 0 comment history adapter.
* @param handle database handle
* @throws DatabaseVersionException if the table was not found
* @param addrMap the address map used to generate keys for addresses
* @param create true if to create a new table; false to load an existing table
* @throws VersionException if the table was not found
* @throws IOException if an error occurred while accessing the database
*/
CommentHistoryAdapterV0(DBHandle handle, AddressMap addrMap, boolean create)
throws VersionException, IOException {
this.addrMap = addrMap;
if (create) {
table =
handle.createTable(COMMENT_HISTORY_TABLE_NAME, COMMENT_HISTORY_SCHEMA,
new int[] { HISTORY_ADDRESS_COL });
table = handle.createTable(COMMENT_HISTORY_TABLE_NAME, COMMENT_HISTORY_SCHEMA,
new int[] { HISTORY_ADDRESS_COL });
}
else {
table = handle.getTable(COMMENT_HISTORY_TABLE_NAME);
@ -59,11 +59,8 @@ class CommentHistoryAdapterV0 extends CommentHistoryAdapter {
userName = SystemUtilities.getUserName();
}
/**
* @see ghidra.program.database.code.CommentHistoryAdapter#createRecord(long, byte, int, int, java.lang.String)
*/
@Override
void createRecord(long addr, byte commentType, int pos1, int pos2, String data)
void createRecord(long addr, byte commentType, int pos1, int pos2, String data, long date)
throws IOException {
Record rec = table.getSchema().createRecord(table.getKey());
@ -73,47 +70,32 @@ class CommentHistoryAdapterV0 extends CommentHistoryAdapter {
rec.setIntValue(HISTORY_POS2_COL, pos2);
rec.setString(HISTORY_STRING_COL, data);
rec.setString(HISTORY_USER_COL, userName);
rec.setLongValue(HISTORY_DATE_COL, System.currentTimeMillis() ); //new Date().getTime());
rec.setLongValue(HISTORY_DATE_COL, date);
table.putRecord(rec);
}
/**
* @see ghidra.program.database.code.CommentHistoryAdapter#getRecordsByAddress(long)
*/
@Override
RecordIterator getRecordsByAddress(Address address) throws IOException {
LongField field = new LongField(addrMap.getKey(address, false));
return table.indexIterator(HISTORY_ADDRESS_COL, field, field, true);
}
/**
* @see ghidra.program.database.code.CommentHistoryAdapter#getAllRecords()
*/
@Override
RecordIterator getAllRecords() throws IOException {
return new AddressKeyRecordIterator(table, addrMap);
}
/**
* @see ghidra.program.database.code.CommentHistoryAdapter#updateRecord(db.Record)
*/
@Override
void updateRecord(Record rec) throws IOException {
table.putRecord(rec);
}
/**
* @see ghidra.program.database.code.CommentHistoryAdapter#deleteRecords(ghidra.program.model.address.Address, ghidra.program.model.address.Address)
*/
@Override
boolean deleteRecords(Address start, Address end) throws IOException {
return AddressRecordDeleter.deleteRecords(table, addrMap, start, end);
}
/**
* @see ghidra.program.database.code.CommentHistoryAdapter#getRecordCount()
*/
@Override
int getRecordCount() {
return table.getRecordCount();

View file

@ -0,0 +1,132 @@
/* ###
* 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 ghidra.program.database.code;
import com.google.common.base.Objects;
/**
* Container object that holds a start and end position within a string. A list of StringDiffs
* is used to keep track of changes made to a string.
*/
public class StringDiff {
/**
* Start position of the string used when text is inserted or replaced
*/
int start;
/**
* End position of the string used when part of the string is replaced
*/
int end;
/**
* String being inserted. This can be an insert or a complete replace (the positions will both
* be -1 in a replace; pos1 will be non-negative during an insert).
*/
public String text;
/**
* Construct a new StringDiff with pos1 and pos2 are initialized to -1
*
* @param newText string
* @return the new diff
*/
public static StringDiff allTextReplaced(String newText) {
return new StringDiff(-1, -1, newText);
}
/**
* Construct a new StringDiff that indicates text was deleted from pos1 to pos2
*
* @param start position 1 for the diff
* @param end position 2 for the diff
* @return the new diff
*/
public static StringDiff textDeleted(int start, int end) {
return new StringDiff(start, end, null);
}
/**
* Construct a new StringDiff that indicates that insertData was inserted at the given position
*
* @param newText inserted string
* @param start position where the text was inserted
* @return the new diff
*/
public static StringDiff textInserted(String newText, int start) {
return new StringDiff(start, -1, newText);
}
// for restoring from saved record
public static StringDiff restore(String text, int start, int end) {
return new StringDiff(start, end, text);
}
private StringDiff(int start, int end, String text) {
this.start = start;
this.end = end;
this.text = text;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((text == null) ? 0 : text.hashCode());
result = prime * result + start;
result = prime * result + end;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
StringDiff other = (StringDiff) obj;
if (!Objects.equal(text, other.text)) {
return false;
}
if (start != other.start) {
return false;
}
if (end != other.end) {
return false;
}
return true;
}
@Override
public String toString() {
if (text != null) {
if (start >= 0) {
return "StringDiff: inserted <" + text + "> at " + start;
}
return "StringDiff: replace with <" + text + ">";
}
return "StringDiff: deleted text from " + start + " to " + end;
}
}

View file

@ -0,0 +1,351 @@
/* ###
* 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 ghidra.program.database.code;
import java.util.*;
import org.apache.commons.lang3.StringUtils;
import generic.algorithms.ReducingListBasedLcs;
class StringDiffUtils {
/**
* Minimum size used to determine whether a new StringDiff object will be
* created just using a string (no positions)
* in the <code>getDiffs(String, String)</code> method.
* @see #getLineDiffs(String, String)
*/
private static int MINIMUM_DIFF_SIZE = 100;
/**
* Returns the list of StringDiff objects that if applied to s1 would result in s2; The
* given text will look only for whole lines using '\n'.
*
* @param s1 the original string
* @param s2 the result string
* this value, then a completely different string will be returned
* @return an array of StringDiff objects that change s1 into s2;
*/
static StringDiff[] getLineDiffs(String s1, String s2) {
return getLineDiffs(s1, s2, MINIMUM_DIFF_SIZE);
}
static StringDiff[] getLineDiffs(String s1, String s2, int minimumDiffSize) {
if (s2.length() < minimumDiffSize) {
return new StringDiff[] { StringDiff.allTextReplaced(s2) };
}
List<Line> aList = split(s1);
List<Line> bList = split(s2);
LineLcs lcs = new LineLcs(aList, bList);
List<Line> commons = lcs.getLcs();
if (commons.isEmpty()) {
// no common text--complete replacement
return new StringDiff[] { StringDiff.allTextReplaced(s2) };
}
int aIndex = 0;
int bIndex = 0;
int aLastIndex = 0;
int bLastIndex = 0;
List<StringDiff> results = new LinkedList<>();
for (Line common : commons) {
aIndex = indexOf(aList, common, aLastIndex);
bIndex = indexOf(bList, common, bLastIndex);
int aDelta = aIndex - aLastIndex;
int bDelta = bIndex - bLastIndex;
int aEnd = aIndex;
int aStart = aEnd - aDelta;
List<Line> aPrevious = aList.subList(aStart, aEnd);
StringDiff delete = createDelete(aPrevious);
if (delete != null) {
results.add(delete);
}
int bEnd = bIndex;
int bStart = bEnd - bDelta;
List<Line> bPrevious = bList.subList(bStart, bEnd);
StringDiff insert = createInsert(bPrevious, charOffset(aList, aIndex));
if (insert != null) {
results.add(insert);
}
// note: nothing is needed for the 'common' string, since we don't track unchanged text
aLastIndex = aIndex + 1;
bLastIndex = bIndex + 1;
}
// grab remainder
StringDiff trailingDeleted = createDeleteAtEnd(aList, aLastIndex, aList.size());
if (trailingDeleted != null) {
results.add(trailingDeleted);
}
StringDiff trailingInserted =
createInsertAtEnd(bList, bLastIndex, bList.size(), s1.length());
if (trailingInserted != null) {
results.add(trailingInserted);
}
return results.toArray(new StringDiff[results.size()]);
}
private static int charOffset(List<Line> list, int index) {
Line line = list.get(index);
return line.start;
}
private static StringDiff createInsertAtEnd(List<Line> list, int start, int end,
int insertIndex) {
if (start - 1 == end) {
return null;
}
List<Line> toDo = list.subList(start, end);
boolean newlineNeeded = true; // we are at the end--need a newline
StringDiff insert = createInsert(toDo, insertIndex, newlineNeeded);
return insert;
}
private static StringDiff createInsert(List<Line> lines, int insertIndex) {
return createInsert(lines, insertIndex, false);
}
private static StringDiff createInsert(List<Line> lines, int insertIndex, boolean isAtEnd) {
if (lines.isEmpty()) {
return null;
}
StringBuilder buffy = new StringBuilder();
// special case: if this insert is for the end of the line, then we want to add
// a newline before the remaining text is added since the original text
// did not have this newline
if (isAtEnd) {
buffy.append('\n');
}
for (Line line : lines) {
buffy.append(line.getText());
}
return StringDiff.textInserted(buffy.toString(), insertIndex);
}
private static StringDiff createDeleteAtEnd(List<Line> list, int start, int end) {
if (start - 1 == end) {
return null;
}
List<Line> toDo = list.subList(start, end);
boolean includeLastNewline = false; // we are at the end--do not include artificial newline
StringDiff delete = createDelete(toDo, includeLastNewline);
return delete;
}
private static StringDiff createDelete(List<Line> lines) {
return createDelete(lines, true);
}
private static StringDiff createDelete(List<Line> lines, boolean includeLastNewline) {
if (lines.isEmpty()) {
return null;
}
int start = 0;
int end = 0;
for (Line line : lines) {
start = line.start;
end = line.start + line.text.length();
}
// special case: if this delete is for the last line, then we want to remove the remaining
// trailing newline
Line last = lines.get(lines.size() - 1);
if (!includeLastNewline && last.isLastLine) {
start -= 1; // remove previous newline
}
return StringDiff.textDeleted(start, end);
}
private static int indexOf(List<Line> list, Line line, int from) {
for (int i = from; i < list.size(); i++) {
if (list.get(i).textMatches(line)) {
return i;
}
}
return list.size(); // should not get here since 's' is known to be in list
}
private static List<Line> split(String s) {
LinkedList<Line> result = new LinkedList<>();
List<String> lines = Arrays.asList(StringUtils.splitPreserveAllTokens(s, '\n'));
int start = 0;
for (String line : lines) {
Line l = new Line(line + '\n', start);
result.add(l);
start += l.text.length();
}
// strip off the trailing newline that we added above
Line last = result.peekLast();
last.markAsLast();
return result;
}
/**
* Applies the array of StringObjects to the string s to produce a new string. Warning - the
* diff objects cannot be applied to an arbitrary string, the Strings must be the original
* String used to compute the diffs.
* @param s the original string
* @param diffs the array of StringDiff object to apply
* @return a new String resulting from applying the diffs to s.
*/
static String applyDiffs(String s, List<StringDiff> diffs) {
if (diffs.isEmpty()) {
return s;
}
if (diffs.get(0).start < 0) {
// all replaced or all deleted
String data = diffs.get(0).text;
return data == null ? "" : data;
}
int pos = 0;
StringBuilder buf = new StringBuilder(s.length());
for (StringDiff element : diffs) {
if (element.start > pos) {
buf.append(s.substring(pos, element.start));
pos = element.start;
}
String data = element.text;
if (data != null) {
buf.append(data);
}
else {
// null data is a delete; move to the end of the delete
pos = element.end;
}
}
if (pos < s.length()) {
buf.append(s.substring(pos));
}
return buf.toString();
}
//==================================================================================================
// Inner Classes
//==================================================================================================
private static class Line {
private String text;
private int start;
private boolean isLastLine;
public Line(String line, int start) {
this.text = line;
this.start = start;
}
String getText() {
if (isLastLine) {
return textWithoutNewline(); // last line and do not include the newline
}
return text;
}
void markAsLast() {
isLastLine = true;
}
private String textWithoutNewline() {
if (text.charAt(text.length() - 1) == '\n') {
return text.substring(0, text.length() - 1);
}
return text;
}
@Override
public String toString() {
return textWithoutNewline() + " @ " + start;
}
boolean textMatches(Line other) {
return Objects.equals(text, other.text);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + start;
result = prime * result + ((text == null) ? 0 : text.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
Line other = (Line) obj;
if (start != other.start) {
return false;
}
if (text == null) {
if (other.text != null) {
return false;
}
}
else if (!text.equals(other.text)) {
return false;
}
return true;
}
}
private static class LineLcs extends ReducingListBasedLcs<Line> {
LineLcs(List<Line> x, List<Line> y) {
super(x, y);
}
@Override
protected boolean matches(Line x, Line y) {
return x.text.equals(y.text);
}
}
}

View file

@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,13 +17,15 @@ package ghidra.program.model.listing;
import java.util.Date;
import org.apache.commons.lang3.StringUtils;
import ghidra.program.model.address.Address;
/**
* Container class for information about changes to a comment.
*/
public class CommentHistory {
private Address addr;
private int commentType;
private Date modificationDate;
@ -39,8 +40,8 @@ public class CommentHistory {
* @param comments the list of comments.
* @param modificationDate the date the comment was changed.
*/
public CommentHistory(Address addr, int commentType, String userName,
String comments, Date modificationDate) {
public CommentHistory(Address addr, int commentType, String userName, String comments,
Date modificationDate) {
this.addr = addr;
this.commentType = commentType;
this.userName = userName;
@ -49,35 +50,55 @@ public class CommentHistory {
}
/**
* Get address for this label history object.
* Get address for this label history object
* @return address for this label history object.
*/
public Address getAddress() {
return addr;
}
/**
* Get the user that made the change.
* Get the user that made the change
* @return the user that made the change
*/
public String getUserName() {
return userName;
}
/**
* Get the comments for this history object.
* Get the comments for this history object
* @return the comments for this history object
*/
public String getComments() {
return comments;
}
/**
* Get the comment type.
* Get the comment type
* @return the comment type
*/
public int getCommentType() {
return commentType;
}
/**
* Get the modification date
* @return the modification date
*/
public Date getModificationDate() {
return modificationDate;
}
@Override
public String toString() {
//@formatter:off
return "{\n" +
"\tuser: " + userName + ",\n" +
"\tdate: " + modificationDate + ",\n" +
"\taddress: " + addr + ",\n" +
"\tcomment: " + StringUtils.abbreviate(comments, 10) + "\n" +
"}";
//@formatter:on
}
}

View file

@ -1,6 +1,5 @@
/* ###
* IP: GHIDRA
* REVIEWED: YES
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,11 +15,11 @@
*/
package ghidra.program.util;
import generic.algorithms.LCS;
import generic.algorithms.Lcs;
import java.util.List;
public class CodeUnitLCS extends LCS<CodeUnitContainer> {
public class CodeUnitLCS extends Lcs<CodeUnitContainer> {
private List<CodeUnitContainer> xList;
private List<CodeUnitContainer> yList;

View file

@ -0,0 +1,203 @@
/* ###
* 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 ghidra.program.database.code;
import static org.junit.Assert.assertEquals;
import java.util.*;
import org.apache.commons.lang3.StringUtils;
import org.junit.Test;
import generic.test.AbstractGTest;
public class StringDiffTest {
@Test
public void testGetDiffLines_Insert_AtFront() {
String[] a1 = new String[] { "This", "is", "four", "friends" };
String[] a2 = new String[] { "Inserted", "This", "is", "four", "friends" };
String v1 = StringUtils.join(a1, '\n');
String v2 = StringUtils.join(a2, '\n');
StringDiff[] diffs = StringDiffUtils.getLineDiffs(v1, v2, 1);
String restoredV2 = StringDiffUtils.applyDiffs(v1, Arrays.asList(diffs));
assertEquals(v2, restoredV2);
}
@Test
public void testGetDiffLines_Insert_AtEnd() {
String[] a1 = new String[] { "This", "is", "four", "friends" };
String[] a2 = new String[] { "This", "is", "four", "friends", "Inserted" };
String v1 = StringUtils.join(a1, '\n');
String v2 = StringUtils.join(a2, '\n');
StringDiff[] diffs = StringDiffUtils.getLineDiffs(v1, v2, 1);
String restoredV2 = StringDiffUtils.applyDiffs(v1, Arrays.asList(diffs));
assertEquals(v2, restoredV2);
}
@Test
public void testGetDiffLines_Insert_AtMiddle() {
String[] a1 = new String[] { "This", "is", "four", "friends" };
String[] a2 = new String[] { "This", "is", "Inserted", "four", "friends" };
String v1 = StringUtils.join(a1, '\n');
String v2 = StringUtils.join(a2, '\n');
StringDiff[] diffs = StringDiffUtils.getLineDiffs(v1, v2, 1);
String restoredV2 = StringDiffUtils.applyDiffs(v1, Arrays.asList(diffs));
assertEquals(v2, restoredV2);
}
@Test
public void testGetDiffLines_Delete_AtStart() {
String[] a1 = new String[] { "DELETED", "This", "is", "the", "best" };
String[] a2 = new String[] { "This", "is", "the", "best" };
String v1 = StringUtils.join(a1, '\n');
String v2 = StringUtils.join(a2, '\n');
StringDiff[] diffs = StringDiffUtils.getLineDiffs(v1, v2, 1);
String restoredV2 = StringDiffUtils.applyDiffs(v1, Arrays.asList(diffs));
assertEquals(v2, restoredV2);
}
@Test
public void testGetDiffLines_Delete_AtEnd() {
String[] a1 = new String[] { "This", "is", "the", "best", "DELETED" };
String[] a2 = new String[] { "This", "is", "the", "best" };
String v1 = StringUtils.join(a1, '\n');
String v2 = StringUtils.join(a2, '\n');
StringDiff[] diffs = StringDiffUtils.getLineDiffs(v1, v2, 1);
String restoredV2 = StringDiffUtils.applyDiffs(v1, Arrays.asList(diffs));
assertEquals(v2, restoredV2);
}
@Test
public void testGetDiffLines_Delete_AtMiddle() {
String[] a1 = new String[] { "This", "is", "DELETED", "the", "best" };
String[] a2 = new String[] { "This", "is", "the", "best" };
String v1 = StringUtils.join(a1, '\n');
String v2 = StringUtils.join(a2, '\n');
StringDiff[] diffs = StringDiffUtils.getLineDiffs(v1, v2, 1);
String restoredV2 = StringDiffUtils.applyDiffs(v1, Arrays.asList(diffs));
assertEquals(v2, restoredV2);
}
@Test
public void testGetDiffLines_Delete_MultipleDeletes() {
String[] a1 = new String[] { "This", "is", "DELETED", "the", "best", "DELETED" };
String[] a2 = new String[] { "This", "is", "the", "best" };
String v1 = StringUtils.join(a1, '\n');
String v2 = StringUtils.join(a2, '\n');
StringDiff[] diffs = StringDiffUtils.getLineDiffs(v1, v2, 1);
String restoredV2 = StringDiffUtils.applyDiffs(v1, Arrays.asList(diffs));
assertEquals(v2, restoredV2);
}
@Test
public void testGetDiffLines_Rearrange_EqualLineLength() {
// note: this text used to cause an infinite loop bug that tripped when two words were
// swapped at some point in the two strings *and* had the same length
String[] a1 = new String[] { "This", "is", "best", "four", "friends" };
String[] a2 = new String[] { "This", "is", "four", "best", "friends" };
String v1 = StringUtils.join(a1, '\n');
String v2 = StringUtils.join(a2, '\n');
StringDiff[] diffs = StringDiffUtils.getLineDiffs(v1, v2, 1);
String restoredV2 = StringDiffUtils.applyDiffs(v1, Arrays.asList(diffs));
assertEquals(v2, restoredV2);
}
@Test
public void testGetDiffLines_Rearrange_DifferentLineLength_LongerThanNewSpot() {
String[] a1 = new String[] { "This", "is", "besties", "four", "friends" };
String[] a2 = new String[] { "This", "is", "four", "besties", "friends" };
String v1 = StringUtils.join(a1, '\n');
String v2 = StringUtils.join(a2, '\n');
StringDiff[] diffs = StringDiffUtils.getLineDiffs(v1, v2, 1);
String restoredV2 = StringDiffUtils.applyDiffs(v1, Arrays.asList(diffs));
assertEquals(v2, restoredV2);
}
@Test
public void testGetDiffLines_Rearrange_DifferentLineLength_ShorterThanNewSpot() {
String[] a1 = new String[] { "This", "is", "be", "four", "friends" };
String[] a2 = new String[] { "This", "is", "four", "be", "friends" };
String v1 = StringUtils.join(a1, '\n');
String v2 = StringUtils.join(a2, '\n');
StringDiff[] diffs = StringDiffUtils.getLineDiffs(v1, v2, 1);
String restoredV2 = StringDiffUtils.applyDiffs(v1, Arrays.asList(diffs));
assertEquals(v2, restoredV2);
}
@Test
public void testReplace() {
String[] a1 = new String[] { "In", "the", "beginning" };
String[] a2 = new String[] { "There", "was", "vastness" };
String v1 = StringUtils.join(a1, '\n');
String v2 = StringUtils.join(a2, '\n');
StringDiff[] diffs = StringDiffUtils.getLineDiffs(v1, v2, 1);
String restoredV2 = StringDiffUtils.applyDiffs(v1, Arrays.asList(diffs));
assertEquals(v2, restoredV2);
}
@Test
public void testTheBiggness_NoOptimization() throws Exception {
List<String> bigLines = generateLines(1200);
List<String> bigLines2 = new ArrayList<>(bigLines);
bigLines2.set(0, "a new line at 0");
bigLines2.set(bigLines2.size() - 1, "a new line at length");
String v1 = StringUtils.join(bigLines, '\n');
String v2 = StringUtils.join(bigLines2, '\n');
StringDiff[] diffs = StringDiffUtils.getLineDiffs(v1, v2, 1);
assertEquals(1, diffs.length); // 1 diff--completely different, due to size restriction on Lcs
String restoredV2 = StringDiffUtils.applyDiffs(v1, Arrays.asList(diffs));
assertEquals(v2, restoredV2);
}
private List<String> generateLines(int size) {
List<String> results = new ArrayList<>();
for (int i = 0; i < size; i++) {
String random = AbstractGTest.getRandomString(0, 50);
random = random.replaceAll("\n", "");
results.add("Line " + (i + 1) + ": " + random);
}
return results;
}
}