/*******************************************************************************
* Copyright (c) 2015 Jeff Martin.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser General Public
* License v3.0 which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl.html
*
* Contributors:
* Jeff Martin - initial API and implementation
******************************************************************************/
package cuchaz.enigma.analysis;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.strobel.decompiler.languages.Region;
import com.strobel.decompiler.languages.java.ast.*;
import cuchaz.enigma.gui.SourceRemapper;
import cuchaz.enigma.translation.mapping.EntryResolver;
import cuchaz.enigma.translation.mapping.ResolutionStrategy;
import cuchaz.enigma.translation.representation.entry.Entry;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class SourceIndex {
private static Pattern ANONYMOUS_INNER = Pattern.compile("\\$\\d+$");
private String source;
private TreeMap, Entry>>> tokenToReference;
private Multimap, Entry>>, Token> referenceToTokens;
private Map, Token> declarationToToken;
private List lineOffsets;
private boolean ignoreBadTokens;
public SourceIndex(String source) {
this(source, true);
}
public SourceIndex(String source, boolean ignoreBadTokens) {
this.source = source;
this.ignoreBadTokens = ignoreBadTokens;
this.tokenToReference = new TreeMap<>();
this.referenceToTokens = HashMultimap.create();
this.declarationToToken = Maps.newHashMap();
calculateLineOffsets();
}
public static SourceIndex buildIndex(String sourceString, CompilationUnit sourceTree, boolean ignoreBadTokens) {
SourceIndex index = new SourceIndex(sourceString, ignoreBadTokens);
sourceTree.acceptVisitor(new SourceIndexVisitor(), index);
return index;
}
private void calculateLineOffsets() {
// count the lines
this.lineOffsets = Lists.newArrayList();
this.lineOffsets.add(0);
for (int i = 0; i < source.length(); i++) {
if (source.charAt(i) == '\n') {
this.lineOffsets.add(i + 1);
}
}
}
public SourceIndex remapTo(SourceRemapper.Result result) {
SourceIndex remapped = new SourceIndex(result.getSource(), ignoreBadTokens);
for (Map.Entry, Token> entry : declarationToToken.entrySet()) {
remapped.declarationToToken.put(entry.getKey(), result.getRemappedToken(entry.getValue()));
}
for (Map.Entry, Entry>>, Collection> entry : referenceToTokens.asMap().entrySet()) {
EntryReference, Entry>> reference = entry.getKey();
Collection oldTokens = entry.getValue();
Collection newTokens = oldTokens.stream()
.map(result::getRemappedToken)
.collect(Collectors.toList());
remapped.referenceToTokens.putAll(reference, newTokens);
}
for (Map.Entry, Entry>>> entry : tokenToReference.entrySet()) {
remapped.tokenToReference.put(result.getRemappedToken(entry.getKey()), entry.getValue());
}
return remapped;
}
public String getSource() {
return this.source;
}
public Token getToken(AstNode node) {
// get the text of the node
String name = "";
if (node instanceof Identifier) {
name = ((Identifier) node).getName();
}
// get a token for this node's region
Region region = node.getRegion();
if (region.getBeginLine() == 0 || region.getEndLine() == 0) {
// DEBUG
System.err.println(String.format("WARNING: %s \"%s\" has invalid region: %s", node.getNodeType(), name, region));
return null;
}
Token token = new Token(toPos(region.getBeginLine(), region.getBeginColumn()), toPos(region.getEndLine(), region.getEndColumn()), this.source);
if (token.start == 0) {
// DEBUG
System.err.println(String.format("WARNING: %s \"%s\" has invalid start: %s", node.getNodeType(), name, region));
return null;
}
if (node instanceof Identifier && name.indexOf('$') >= 0 && node.getParent() instanceof ConstructorDeclaration && name.lastIndexOf('$') >= 0 && !ANONYMOUS_INNER.matcher(name).matches()) {
TypeDeclaration type = node.getParent().getParent() instanceof TypeDeclaration ? (TypeDeclaration) node.getParent().getParent() : null;
if (type != null) {
name = type.getName();
token.end = token.start + name.length();
}
}
// DEBUG
// System.out.println( String.format( "%s \"%s\" region: %s", node.getNodeType(), name, region ) );
// if the token has a $ in it, something's wrong. Ignore this token
if (name.lastIndexOf('$') >= 0 && this.ignoreBadTokens) {
// DEBUG
System.err.println(String.format("WARNING: %s \"%s\" is probably a bad token. It was ignored", node.getNodeType(), name));
return null;
}
return token;
}
public void addReference(AstNode node, Entry> deobfEntry, Entry> deobfContext) {
Token token = getToken(node);
if (token != null) {
EntryReference, Entry>> deobfReference = new EntryReference<>(deobfEntry, token.text, deobfContext);
this.tokenToReference.put(token, deobfReference);
this.referenceToTokens.put(deobfReference, token);
}
}
public void addDeclaration(AstNode node, Entry> deobfEntry) {
Token token = getToken(node);
if (token != null) {
EntryReference, Entry>> reference = new EntryReference<>(deobfEntry, token.text);
this.tokenToReference.put(token, reference);
this.referenceToTokens.put(reference, token);
this.declarationToToken.put(deobfEntry, token);
}
}
public Token getReferenceToken(int pos) {
Token token = this.tokenToReference.floorKey(new Token(pos, pos, null));
if (token != null && token.contains(pos)) {
return token;
}
return null;
}
public Collection getReferenceTokens(EntryReference, Entry>> deobfReference) {
return this.referenceToTokens.get(deobfReference);
}
@Nullable
public EntryReference, Entry>> getReference(Token token) {
if (token == null) {
return null;
}
return this.tokenToReference.get(token);
}
public Iterable referenceTokens() {
return this.tokenToReference.keySet();
}
public Iterable declarationTokens() {
return this.declarationToToken.values();
}
public Iterable> declarations() {
return this.declarationToToken.keySet();
}
public Token getDeclarationToken(Entry> entry) {
return this.declarationToToken.get(entry);
}
public int getLineNumber(int pos) {
// line number is 1-based
int line = 0;
for (Integer offset : this.lineOffsets) {
if (offset > pos) {
break;
}
line++;
}
return line;
}
public int getColumnNumber(int pos) {
// column number is 1-based
return pos - this.lineOffsets.get(getLineNumber(pos) - 1) + 1;
}
private int toPos(int line, int col) {
// line and col are 1-based
return this.lineOffsets.get(line - 1) + col - 1;
}
public void resolveReferences(EntryResolver resolver) {
// resolve all the classes in the source references
for (Token token : Lists.newArrayList(referenceToTokens.values())) {
EntryReference, Entry>> reference = tokenToReference.get(token);
EntryReference, Entry>> resolvedReference = resolver.resolveFirstReference(reference, ResolutionStrategy.RESOLVE_CLOSEST);
// replace the reference
tokenToReference.replace(token, resolvedReference);
Collection tokens = referenceToTokens.removeAll(reference);
referenceToTokens.putAll(resolvedReference, tokens);
}
}
}