// 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.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ThreadPoolExecutor;
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.DecNumber;
import org.infinity.datatype.Flag;
import org.infinity.datatype.ResourceRef;
import org.infinity.datatype.SectionCount;
import org.infinity.datatype.SectionOffset;
import org.infinity.datatype.TextString;
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.key.ResourceEntry;
import org.infinity.resource.wed.Overlay;
import org.infinity.resource.wed.Tilemap;
import org.infinity.util.Debugging;
import org.infinity.util.Misc;
public final class StructChecker extends ChildFrame implements ActionListener, Runnable,
ListSelectionListener
{
private static final String FMT_PROGRESS = "Checking %ss...";
private static final String[] FILETYPES = {"ARE", "CHR", "CHU", "CRE", "DLG", "EFF", "GAM", "ITM",
"PRO", "SPL", "STO", "VEF", "VVC", "WED", "WMP"};
private static final HashMap<String, StructInfo> fileInfo = new HashMap<String, StructInfo>();
static {
fileInfo.put("ARE", new StructInfo("AREA", new String[]{"V1.0", "V9.1"}));
fileInfo.put("CHR", new StructInfo("CHR ", new String[]{"V1.0", "V1.2", "V2.0", "V2.1", "V2.2", "V9.0"}));
fileInfo.put("CHU", new StructInfo("CHUI", new String[]{"V1 "}));
fileInfo.put("CRE", new StructInfo("CRE ", new String[]{"V1.0", "V1.1", "V1.2", "V2.2", "V9.0"}));
fileInfo.put("DLG", new StructInfo("DLG ", new String[]{"V1.0"}));
fileInfo.put("EFF", new StructInfo("EFF ", new String[]{"V2.0"}));
fileInfo.put("GAM", new StructInfo("GAME", new String[]{"V1.1", "V2.0", "V2.1", "V2.2"}));
fileInfo.put("ITM", new StructInfo("ITM ", new String[]{"V1 ", "V1.1", "V2.0"}));
fileInfo.put("PRO", new StructInfo("PRO ", new String[]{"V1.0"}));
fileInfo.put("SPL", new StructInfo("SPL ", new String[]{"V1 ", "V2.0"}));
fileInfo.put("STO", new StructInfo("STOR", new String[]{"V1.0", "V1.1", "V9.0"}));
fileInfo.put("VEF", new StructInfo("VEF ", new String[]{"V1.0"}));
fileInfo.put("VVC", new StructInfo("VVC ", new String[]{"V1.0"}));
fileInfo.put("WED", new StructInfo("WED ", new String[]{"V1.3"}));
fileInfo.put("WMP", new StructInfo("WMAP", new String[]{"V1.0"}));
}
private final ChildFrame resultFrame = new ChildFrame("Corrupted files 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 final SortableTable table;
private ProgressMonitor progress;
private int progressIndex;
public StructChecker()
{
super("Find Corrupted Files");
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", "Error message"}),
colClasses, Arrays.asList(new Integer[]{50, 50, 400}));
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();
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 box : boxes)
box.setSelected(!box.isSelected());
}
else if (event.getSource() == bcancel)
setVisible(false);
else if (event.getSource() == bopen) {
int row = table.getSelectedRow();
if (row != -1) {
ResourceEntry resourceEntry = (ResourceEntry)table.getValueAt(row, 0);
NearInfinity.getInstance().showResourceEntry(resourceEntry);
}
}
else if (event.getSource() == bopennew) {
int row = table.getSelectedRow();
if (row != -1) {
ResourceEntry resourceEntry = (ResourceEntry)table.getValueAt(row, 0);
new ViewFrame(resultFrame, ResourceFactory.getResource(resourceEntry));
}
}
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("File corruption 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 {
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);
((AbstractStruct)resource).getViewer().selectEntry((String)table.getValueAt(row, 1));
}
}
}
});
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 search(ResourceEntry entry, AbstractStruct struct)
{
List<StructEntry> flatList = struct.getFlatList();
if (flatList.size() < 2) {
return;
}
StructEntry entry1 = flatList.get(0);
int offset = entry1.getOffset() + entry1.getSize();
for (int i = 1; i < flatList.size(); i++) {
StructEntry entry2 = flatList.get(i);
if (!entry2.getName().equals(AbstractStruct.COMMON_UNUSED_BYTES)) {
int delta = entry2.getOffset() - offset;
if (entry2.getSize() > 0 && delta < 0) {
synchronized (table) {
table.addTableItem(new Corruption(entry, entry1.getOffset(),
entry1.getName() + '(' + Integer.toHexString(entry1.getOffset()) +
"h)" +
" overlaps " +
entry2.getName() + '(' + Integer.toHexString(entry2.getOffset()) +
"h)" +
" by " + -delta + " bytes"));
}
} else if (delta > 0) {
synchronized (table) {
table.addTableItem(new Corruption(entry, entry1.getOffset(),
delta + " unused bytes between " +
entry1.getName() + '(' + Integer.toHexString(entry1.getOffset()) +
"h)" +
" and " +
entry2.getName() + '(' + Integer.toHexString(entry2.getOffset()) +
"h)"));
}
}
// Using max() as shared data regions may confuse the consistency check algorithm
offset = Math.max(offset, entry2.getOffset() + entry2.getSize());
entry1 = entry2;
}
}
StructEntry last = flatList.get(flatList.size() - 1);
if (last.getName().equals(AbstractStruct.COMMON_UNUSED_BYTES)) {
synchronized (table) {
table.addTableItem(new Corruption(entry, last.getOffset(),
last.getSize() + " unused bytes after " +
entry1.getName() + '(' + Integer.toHexString(entry1.getOffset()) +
"h)"));
}
}
// Checking signature and version fields
StructInfo info = fileInfo.get(entry.getExtension());
if (info != null) {
String sig = ((TextString)struct.getAttribute(AbstractStruct.COMMON_SIGNATURE)).toString();
if (info.isSignature(sig)) {
String ver = ((TextString)struct.getAttribute(AbstractStruct.COMMON_VERSION)).toString();
if (!info.isVersion(ver)) {
// invalid version?
synchronized (table) {
table.addTableItem(new Corruption(entry, 4, "Unsupported or invalid version: \"" + ver + "\""));
}
}
} else {
// invalid signature?
synchronized (table) {
table.addTableItem(new Corruption(entry, 0, "Invalid signature: \"" + sig + "\""));
}
}
}
// Type-specific checks
if (entry.getExtension().equalsIgnoreCase("WED")) {
List<Corruption> list = getWedCorruption(entry, struct);
for (Iterator<Corruption> iter = list.iterator(); iter.hasNext();) {
synchronized (table) {
table.addTableItem(iter.next());
}
}
}
}
// Checking for WED-specific corruptions
private List<Corruption> getWedCorruption(ResourceEntry entry, AbstractStruct struct)
{
List<Corruption> list = new ArrayList<Corruption>();
if (entry.getExtension().equalsIgnoreCase("WED")) {
final int ovlSize = 0x18; // size of an Overlay structure
int ovlCount = ((SectionCount)struct.getAttribute(8, false)).getValue(); // # overlays
int ovlStartOfs = ((SectionOffset)struct.getAttribute(16, false)).getValue(); // Overlays offset
for (int ovlIdx = 0; ovlIdx < ovlCount; ovlIdx++) {
int ovlOfs = ovlStartOfs + ovlIdx*ovlSize;
Overlay overlay = (Overlay)struct.getAttribute(ovlOfs, false); // Overlay
if (overlay == null) {
continue;
}
int width = ((DecNumber)overlay.getAttribute(ovlOfs + 0, false)).getValue();
int height = ((DecNumber)overlay.getAttribute(ovlOfs + 2, false)).getValue();
String tisName = ((ResourceRef)overlay.getAttribute(ovlOfs + 4, false)).getResourceName();
int tileStartOfs = ((SectionOffset)overlay.getAttribute(ovlOfs + 16, false)).getValue();
int indexStartOfs = ((SectionOffset)overlay.getAttribute(ovlOfs + 20, false)).getValue();
if (tisName == null || tisName.isEmpty() || !ResourceFactory.resourceExists(tisName)) {
continue;
}
// checking Overlay fields
boolean skip = false;
if (width <= 0) {
list.add(new Corruption(entry, ovlOfs + 0,
String.format("Overlay %1$d: Tileset width is <= 0", ovlIdx)));
skip = true;
}
if (height <= 0) {
list.add(new Corruption(entry, ovlOfs + 2,
String.format("Overlay %1$d: Tileset height is <= 0", ovlIdx)));
skip = true;
}
if ((tileStartOfs <= ovlOfs + ovlCount*ovlSize) || (tileStartOfs >= struct.getSize())) {
list.add(new Corruption(entry, ovlOfs + 16,
String.format("Overlay %1$d: Tilemap offset is invalid", ovlIdx)));
skip = true;
}
if ((indexStartOfs < ovlOfs + ovlCount*ovlSize) || (indexStartOfs >= struct.getSize())) {
list.add(new Corruption(entry, ovlOfs + 16,
String.format("Overlay %1$d: Tilemap lookup offset is invalid", ovlIdx)));
skip = true;
}
if (skip) {
continue;
}
// Checking Tilemap fields
ResourceEntry tisResource = ResourceFactory.getResourceEntry(tisName);
int[] tisInfo; // = {tileCount, tileSize}
try {
tisInfo = tisResource.getResourceInfo();
} catch (Exception e) {
tisInfo = null;
}
if (tisInfo == null || tisInfo.length < 2) {
continue;
}
final int tileSize = 0x0a; // size of a Tilemap structure
int numTiles = width*height;
int tileEndOfs = tileStartOfs + numTiles*tileSize;
int indexEndOfs = indexStartOfs + 2*numTiles;
// caching tile maps and tile lookup indices
HashMap<Integer, Tilemap> mapTiles = new HashMap<Integer, Tilemap>(numTiles*3/2, 0.8f);
HashMap<Integer, Integer> mapIndices = new HashMap<Integer, Integer>(numTiles*3/2, 0.8f);
for (Iterator<StructEntry> iter = overlay.getList().iterator(); iter.hasNext();) {
StructEntry item = iter.next();
int curOfs = item.getOffset();
if (curOfs >= tileStartOfs && curOfs < tileEndOfs && item instanceof Tilemap) {
int index = (curOfs - tileStartOfs) / item.getSize();
mapTiles.put(Integer.valueOf(index), (Tilemap)item);
} else if (item.getOffset() > indexStartOfs && curOfs < indexEndOfs && item instanceof DecNumber) {
int index = (curOfs - indexStartOfs) / 2;
mapIndices.put(Integer.valueOf(index), Integer.valueOf(((DecNumber)item).getValue()));
}
}
// checking indices
for (int i = 0; i < numTiles; i++) {
Tilemap tile = mapTiles.get(Integer.valueOf(i));
if (tile != null) {
int tileOfs = tile.getOffset();
int tileIdx = (tileOfs - tileStartOfs) / tileSize;
int tileIdxPri = ((DecNumber)tile.getAttribute(tileOfs + 0, false)).getValue();
int tileCountPri = ((DecNumber)tile.getAttribute(tileOfs + 2, false)).getValue();
int tileIdxSec = ((DecNumber)tile.getAttribute(tileOfs + 4, false)).getValue();
Flag tileFlag = (Flag)tile.getAttribute(tileOfs + 6, false);
int tileFlagValue = (int)tileFlag.getValue();
for (int j = tileIdxPri, count = tileIdxPri + tileCountPri; j < count; j++) {
Integer tileLookupIndex = mapIndices.get(Integer.valueOf(j));
if (tileLookupIndex != null) {
if (tileLookupIndex >= tisInfo[0]) {
list.add(new Corruption(entry, tileOfs + 0,
String.format("Overlay %1$d/Tilemap %2$d: Primary tile index %3$d " +
"out of range [0..%4$d]",
ovlIdx, tileIdx, j, tisInfo[0] - 1)));
}
}
}
if (tileFlagValue > 0 && tileIdxSec >= tisInfo[0]) {
list.add(new Corruption(entry, tileOfs + 4,
String.format("Overlay %1$d/Tilemap %2$d: Secondary tile index %3$d " +
"out of range [0..%4$d]",
ovlIdx, tileIdx, tileIdxSec, tisInfo[0] - 1)));
}
}
}
}
}
return list;
}
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 Corruption implements TableItem
{
private final ResourceEntry resourceEntry;
private final String offset;
private final String errorMsg;
private Corruption(ResourceEntry resourceEntry, int offset, String errorMsg)
{
this.resourceEntry = resourceEntry;
this.offset = Integer.toHexString(offset) + 'h';
this.errorMsg = errorMsg;
}
@Override
public Object getObjectAt(int columnIndex)
{
if (columnIndex == 0)
return resourceEntry;
else if (columnIndex == 1)
return offset;
else
return errorMsg;
}
@Override
public String toString()
{
StringBuffer buf = new StringBuffer("File: ");
buf.append(resourceEntry.toString());
buf.append(" Offset: ").append(offset);
buf.append(" Error message: ").append(errorMsg);
return buf.toString();
}
}
// Stores supported signature and versions for a single structured resource format
private static final class StructInfo
{
public final String signature;
public final String[] version;
public StructInfo(String sig, String[] ver)
{
signature = (sig != null) ? sig : "";
if (ver != null) {
version = new String[ver.length];
for (int i = 0; i < version.length; i++) {
version[i] = (ver[i] != null) ? ver[i] : "";
}
} else {
version = new String[0];
}
}
/** Returns whether the signatures matches the signature of the current structure definition. */
public boolean isSignature(String sig)
{
return (sig != null) ? signature.equals(sig) : false;
}
/** Returns whether the specified version is supported by the current structure definition. */
public boolean isVersion(String ver)
{
if (ver != null) {
for (final String v: version) {
if (ver.equals(v)) {
return true;
}
}
}
return false;
}
}
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 != null) {
search(entry, (AbstractStruct)resource);
}
}
advanceProgress(false);
}
}
}