// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.check;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ProgressMonitor;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.infinity.NearInfinity;
import org.infinity.datatype.StringRef;
import org.infinity.gui.BrowserMenuBar;
import org.infinity.gui.Center;
import org.infinity.gui.ChildFrame;
import org.infinity.gui.SortableTable;
import org.infinity.gui.TableItem;
import org.infinity.gui.ViewFrame;
import org.infinity.icon.Icons;
import org.infinity.resource.AbstractStruct;
import org.infinity.resource.Profile;
import org.infinity.resource.Resource;
import org.infinity.resource.ResourceFactory;
import org.infinity.resource.StructEntry;
import org.infinity.resource.TextResource;
import org.infinity.resource.are.AutomapNote;
import org.infinity.resource.bcs.BcsResource;
import org.infinity.resource.bcs.Compiler;
import org.infinity.resource.bcs.Decompiler;
import org.infinity.resource.dlg.AbstractCode;
import org.infinity.resource.dlg.Action;
import org.infinity.resource.dlg.DlgResource;
import org.infinity.resource.gam.JournalEntry;
import org.infinity.resource.key.ResourceEntry;
import org.infinity.resource.text.PlainTextResource;
import org.infinity.util.Debugging;
import org.infinity.util.Misc;
import org.infinity.util.StringResource;
public class StrrefIndexChecker extends ChildFrame implements ActionListener, ListSelectionListener,
Runnable
{
private static final String FMT_PROGRESS = "Checking %ss...";
private static final String[] FILETYPES = {"2DA", "ARE", "BCS", "BS", "CHR", "CHU", "CRE", "DLG",
"EFF", "GAM", "INI", "ITM", "SPL", "SRC", "STO", "WMP"};
private final ChildFrame resultFrame = new ChildFrame("Illegal strrefs found", true);
private final JButton bstart = new JButton("Check", Icons.getIcon(Icons.ICON_FIND_16));
private final JButton bcancel = new JButton("Cancel", Icons.getIcon(Icons.ICON_DELETE_16));
private final JButton binvert = new JButton("Invert", Icons.getIcon(Icons.ICON_REFRESH_16));
private final JButton bopen = new JButton("Open", Icons.getIcon(Icons.ICON_OPEN_16));
private final JButton bopennew = new JButton("Open in new window", Icons.getIcon(Icons.ICON_OPEN_16));
private final JButton bsave = new JButton("Save...", Icons.getIcon(Icons.ICON_SAVE_16));
private final JCheckBox[] boxes = new JCheckBox[FILETYPES.length];
private final List<ResourceEntry> files = new ArrayList<ResourceEntry>();
private SortableTable table;
private int strrefCount;
private ProgressMonitor progress;
private int progressIndex;
public StrrefIndexChecker()
{
super("Find illegal strrefs");
setIconImage(Icons.getIcon(Icons.ICON_REFRESH_16).getImage());
List<Class<? extends Object>> colClasses = new ArrayList<Class<? extends Object>>(3);
colClasses.add(Object.class); colClasses.add(Object.class); colClasses.add(Object.class);
table = new SortableTable(Arrays.asList(new String[]{"File", "Offset / Line:Pos", "Strref"}),
colClasses, Arrays.asList(new Integer[]{200, 100, 100}));
bstart.setMnemonic('s');
bcancel.setMnemonic('c');
binvert.setMnemonic('i');
bstart.addActionListener(this);
bcancel.addActionListener(this);
binvert.addActionListener(this);
getRootPane().setDefaultButton(bstart);
JPanel boxpanel = new JPanel(new GridLayout(0, 2, 3, 3));
for (int i = 0; i < boxes.length; i++) {
boxes[i] = new JCheckBox(FILETYPES[i], true);
boxpanel.add(boxes[i]);
}
boxpanel.setBorder(BorderFactory.createEmptyBorder(3, 12, 3, 0));
JPanel ipanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
ipanel.add(binvert);
JPanel innerpanel = new JPanel(new BorderLayout());
innerpanel.add(boxpanel, BorderLayout.CENTER);
innerpanel.add(ipanel, BorderLayout.SOUTH);
innerpanel.setBorder(BorderFactory.createTitledBorder("Select files to check:"));
JPanel bpanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
bpanel.add(bstart);
bpanel.add(bcancel);
JPanel mainpanel = new JPanel(new BorderLayout());
mainpanel.add(innerpanel, BorderLayout.CENTER);
mainpanel.add(bpanel, BorderLayout.SOUTH);
mainpanel.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
JPanel pane = (JPanel)getContentPane();
pane.setLayout(new BorderLayout());
pane.add(mainpanel, BorderLayout.CENTER);
pack();
setMinimumSize(getPreferredSize());
Center.center(this, NearInfinity.getInstance().getBounds());
setVisible(true);
}
//--------------------- Begin Interface ActionListener ---------------------
@Override
public void actionPerformed(ActionEvent event)
{
if (event.getSource() == bstart) {
setVisible(false);
for (int i = 0; i < FILETYPES.length; i++) {
if (boxes[i].isSelected()) {
files.addAll(ResourceFactory.getResources(FILETYPES[i]));
}
}
if (files.size() > 0) {
new Thread(this).start();
}
} else if (event.getSource() == binvert) {
for (final JCheckBox cb: boxes) {
cb.setSelected(!cb.isSelected());
}
} else if (event.getSource() == bcancel) {
setVisible(false);
} else if (event.getSource() == bopen) {
int row = table.getSelectedRow();
if (row >= 0) {
ResourceEntry entry = (ResourceEntry)table.getValueAt(row, 0);
NearInfinity.getInstance().showResourceEntry(entry);
}
} else if (event.getSource() == bopennew) {
int row = table.getSelectedRow();
if (row >= 0) {
ResourceEntry entry = (ResourceEntry)table.getValueAt(row, 0);
new ViewFrame(resultFrame, ResourceFactory.getResource(entry));
}
} else if (event.getSource() == bsave) {
JFileChooser chooser = new JFileChooser(Profile.getGameRoot().toFile());
chooser.setDialogTitle("Save result");
chooser.setSelectedFile(new File(chooser.getCurrentDirectory(), "result.txt"));
if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
Path output = chooser.getSelectedFile().toPath();
if (Files.exists(output)) {
String[] options = {"Overwrite", "Cancel"};
if (JOptionPane.showOptionDialog(this, output + " exists. Overwrite?",
"Save result", JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE, null, options, options[0]) != 0)
return;
}
try (BufferedWriter bw = Files.newBufferedWriter(output)) {
bw.write("Illegal strref search"); bw.newLine();
bw.write("Number of errors: " + table.getRowCount()); bw.newLine();
for (int i = 0; i < table.getRowCount(); i++) {
bw.write(table.getTableItemAt(i).toString()); bw.newLine();
}
JOptionPane.showMessageDialog(this, "Result saved to " + output, "Save complete",
JOptionPane.INFORMATION_MESSAGE);
} catch (IOException e) {
JOptionPane.showMessageDialog(this, "Error while saving " + output,"Error",
JOptionPane.ERROR_MESSAGE);
e.printStackTrace();
}
}
}
}
//--------------------- End Interface ActionListener ---------------------
//--------------------- Begin Interface ListSelectionListener ---------------------
@Override
public void valueChanged(ListSelectionEvent event)
{
bopen.setEnabled(true);
bopennew.setEnabled(true);
}
//--------------------- End Interface ListSelectionListener ---------------------
//--------------------- Begin Interface Runnable ---------------------
@Override
public void run()
{
try {
StringResource.getStringRef(0);
strrefCount = StringResource.getMaxIndex();
String type = "WWWW";
progressIndex = 0;
progress = new ProgressMonitor(NearInfinity.getInstance(), "Checking...",
String.format(FMT_PROGRESS, type),
0, files.size());
progress.setMillisToDecideToPopup(100);
ThreadPoolExecutor executor = Misc.createThreadPool();
boolean isCancelled = false;
Debugging.timerReset();
for (int i = 0; i < files.size(); i++) {
ResourceEntry entry = files.get(i);
if (i % 10 == 0) {
String ext = entry.getExtension();
if (ext != null && !type.equalsIgnoreCase(ext)) {
type = ext;
progress.setNote(String.format(FMT_PROGRESS, type));
}
}
Misc.isQueueReady(executor, true, -1);
executor.execute(new Worker(entry));
if (progress.isCanceled()) {
isCancelled = true;
break;
}
}
// enforcing thread termination if process has been cancelled
if (isCancelled) {
executor.shutdownNow();
} else {
executor.shutdown();
}
// waiting for pending threads to terminate
while (!executor.isTerminated()) {
if (!isCancelled && progress.isCanceled()) {
executor.shutdownNow();
isCancelled = true;
}
try { Thread.sleep(1); } catch (InterruptedException e) {}
}
if (isCancelled) {
resultFrame.close();
JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Check canceled", "Info",
JOptionPane.INFORMATION_MESSAGE);
return;
}
if (table.getRowCount() == 0) {
JOptionPane.showMessageDialog(NearInfinity.getInstance(), "No errors found",
"Info", JOptionPane.INFORMATION_MESSAGE);
} else {
table.tableComplete();
resultFrame.setIconImage(Icons.getIcon(Icons.ICON_REFRESH_16).getImage());
JLabel count = new JLabel(table.getRowCount() + " error(s) found", JLabel.CENTER);
count.setFont(count.getFont().deriveFont((float)count.getFont().getSize() + 2.0f));
bopen.setMnemonic('o');
bopennew.setMnemonic('n');
bsave.setMnemonic('s');
resultFrame.getRootPane().setDefaultButton(bopennew);
JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER));
panel.add(bopen);
panel.add(bopennew);
panel.add(bsave);
JScrollPane scrollTable = new JScrollPane(table);
scrollTable.getViewport().setBackground(table.getBackground());
JPanel pane = (JPanel)resultFrame.getContentPane();
pane.setLayout(new BorderLayout(0, 3));
pane.add(count, BorderLayout.NORTH);
pane.add(scrollTable, BorderLayout.CENTER);
pane.add(panel, BorderLayout.SOUTH);
bopen.setEnabled(false);
bopennew.setEnabled(false);
table.setFont(BrowserMenuBar.getInstance().getScriptFont());
table.getSelectionModel().addListSelectionListener(this);
table.addMouseListener(new MouseAdapter()
{
@Override
public void mouseReleased(MouseEvent event)
{
if (event.getClickCount() == 2) {
int row = table.getSelectedRow();
if (row != -1) {
ResourceEntry resourceEntry = (ResourceEntry)table.getValueAt(row, 0);
Resource resource = ResourceFactory.getResource(resourceEntry);
new ViewFrame(resultFrame, resource);
StrrefEntry item = (StrrefEntry)table.getTableItemAt(row);
if (item.isText) {
((TextResource)resource).highlightText(item.line, Integer.toString(item.strref));
} else {
((AbstractStruct)resource).getViewer().selectEntry(item.offset);
}
}
}
}
});
bopen.addActionListener(this);
bopennew.addActionListener(this);
bsave.addActionListener(this);
pane.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
resultFrame.setSize(700, 600);
Center.center(resultFrame, NearInfinity.getInstance().getBounds());
resultFrame.setVisible(true);
}
} finally {
advanceProgress(true);
}
Debugging.timerShow("Check completed", Debugging.TimeFormat.MILLISECONDS);
}
//--------------------- End Interface Runnable ---------------------
private void checkDialog(DlgResource dialog)
{
if (dialog != null) {
List<StructEntry> flatList = dialog.getFlatList();
for (final StructEntry entry: flatList) {
if (entry instanceof StringRef) {
int strref = ((StringRef)entry).getValue();
if (strref < -1 || strref >= strrefCount) {
synchronized (table) {
table.addTableItem(new StrrefEntry(dialog.getResourceEntry(), entry.getOffset(), strref));
}
}
} else if (entry instanceof AbstractCode) {
AbstractCode code = (AbstractCode)entry;
try {
Compiler compiler = new Compiler(code.toString(),
(code instanceof Action) ? Compiler.ScriptType.ACTION
: Compiler.ScriptType.TRIGGER);
String compiled = compiler.getCode();
Decompiler decompiler = new Decompiler(compiled, true);
if (code instanceof Action) {
decompiler.setScriptType(Decompiler.ScriptType.ACTION);
} else {
decompiler.setScriptType(Decompiler.ScriptType.TRIGGER);
}
decompiler.decompile();
Set<Integer> used = decompiler.getStringRefsUsed();
for (final Integer stringRef : used) {
int strref = stringRef.intValue();
if (strref < -1 || strref >= strrefCount) {
synchronized (table) {
table.addTableItem(new StrrefEntry(dialog.getResourceEntry(), entry.getOffset(), strref));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
private void checkScript(BcsResource script)
{
if (script != null) {
Decompiler decompiler = new Decompiler(script.getCode(), true);
decompiler.decompile();
Set<Integer> used = decompiler.getStringRefsUsed();
for (final Integer stringRef : used) {
int strref = stringRef.intValue();
if (strref < -1 || strref >= strrefCount) {
// XXX: search routine may produce false positives
String strrefString = stringRef.toString();
String source = decompiler.getSource();
String[] lines = source.split("\r?\n");
int line = -1, pos = -1, len = -1;
Pattern pattern = Pattern.compile("\\b" + strrefString + "\\b", Pattern.DOTALL);
for (int i = 0; i < lines.length; i++) {
Matcher matcher = pattern.matcher(lines[i]);
if (matcher.find()) {
line = i;
pos = matcher.start();
len = matcher.end() - pos;
break;
}
}
synchronized (table) {
table.addTableItem(new StrrefEntry(script.getResourceEntry(), line + 1, pos + 1, len, strref));
}
}
}
}
}
private void checkStruct(AbstractStruct struct)
{
if (struct != null) {
List<StructEntry> flatList = struct.getFlatList();
for (final StructEntry entry: flatList) {
if (entry instanceof StringRef) {
int strref = ((StringRef)entry).getValue();
if (strref < -1 || strref >= strrefCount) {
if (strref >= 3000000 &&
(entry.getParent() instanceof AutomapNote || entry.getParent() instanceof JournalEntry)) {
// skip talk override entries
continue;
}
synchronized (table) {
table.addTableItem(new StrrefEntry(struct.getResourceEntry(), entry.getOffset(), strref));
}
}
}
}
}
}
private void checkText(PlainTextResource text)
{
if (text != null) {
Pattern pattern = Pattern.compile("\\b\\d+\\b", Pattern.DOTALL);
String[] lines = text.getText().split("\r?\n");
for (int i = 0; i < lines.length; i++) {
Matcher matcher = pattern.matcher(lines[i]);
while (matcher.find()) {
int line = i;
int pos = matcher.start();
int len = matcher.end() - pos;
try {
long strref = Long.parseLong(lines[line].substring(pos, pos + len));
// skip values out of integer range
if (strref >= Integer.MIN_VALUE && strref <= Integer.MAX_VALUE) {
if (strref < -1 || strref > strrefCount) {
synchronized (table) {
table.addTableItem(new StrrefEntry(text.getResourceEntry(), line + 1, pos + 1, len, (int)strref));
}
}
}
} catch (NumberFormatException e) {
e.printStackTrace();
}
}
}
}
}
private synchronized void advanceProgress(boolean finished)
{
if (progress != null) {
if (finished) {
progressIndex = 0;
progress.close();
progress = null;
} else {
progressIndex++;
progress.setProgress(progressIndex);
}
}
}
//-------------------------- INNER CLASSES --------------------------
private static final class StrrefEntry implements TableItem
{
private final boolean isText;
private final ResourceEntry entry;
private final int offset;
private final int line, pos;
private final int strref;
/** Constructor for text resources (2DA, BCS, ...). */
public StrrefEntry(ResourceEntry entry, int line, int pos, int len, int strref)
{
this.isText = true;
this.entry = entry;
this.line = line;
this.pos = pos;
this.strref = strref;
this.offset = -1;
}
/** Constructor for structured resources. */
public StrrefEntry(ResourceEntry entry, int offset, int strref)
{
this.isText = false;
this.entry = entry;
this.offset = offset;
this.strref = strref;
this.line = this.pos = -1;
}
@Override
public Object getObjectAt(int columnIndex)
{
switch (columnIndex) {
case 0: return entry;
case 1: return isText ? (Integer.toString(line) + ":" + Integer.toString(pos)) : Integer.toHexString(offset) + 'h';
default: return Integer.toString(strref);
}
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder("File: ");
sb.append(entry.toString());
if (isText) {
sb.append(" Line: ").append(line);
sb.append(" Position: ").append(pos);
} else {
sb.append(" Offset: ").append(Integer.toHexString(offset)).append('h');
}
sb.append(" Strref: ").append(strref);
return sb.toString();
}
}
private class Worker implements Runnable
{
private final ResourceEntry entry;
public Worker(ResourceEntry entry)
{
this.entry = entry;
}
@Override
public void run()
{
if (entry != null) {
Resource resource = ResourceFactory.getResource(entry);
if (resource instanceof DlgResource) {
checkDialog((DlgResource)resource);
} else if (resource instanceof BcsResource) {
checkScript((BcsResource)resource);
} else if (resource instanceof PlainTextResource) {
checkText((PlainTextResource)resource);
} else if (resource instanceof AbstractStruct) {
checkStruct((AbstractStruct)resource);
}
}
advanceProgress(false);
}
}
}