Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix EnigmaMappings parsing #903

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
import software.coley.recaf.services.mapping.data.ClassMapping;
import software.coley.recaf.services.mapping.data.FieldMapping;
import software.coley.recaf.services.mapping.data.MethodMapping;
import software.coley.recaf.util.StringUtil;

import java.util.Stack;
import java.util.ArrayDeque;
import java.util.Deque;

/**
* Enigma mappings file implementation.
Expand All @@ -24,7 +24,15 @@
public class EnigmaMappings extends AbstractMappingFileFormat {
public static final String NAME = "Enigma";
private static final String FAIL = "Invalid Enigma mappings, ";
private final Logger logger = Logging.get(EnigmaMappings.class);
private static final Logger LOGGER = Logging.get(EnigmaMappings.class);
// parser phase constants
private static final int PHASE_IGNORE_LINE = 0;
private static final int PHASE_FIND_TYPE = 1;
private static final int PHASE_TYPE_CLASS = 2;
private static final int PHASE_TYPE_FIELD = 3;
private static final int PHASE_TYPE_METHOD = 4;
// needs to be higher than highest phase, as it is an additive flag
private static final int PHASE_TYPE_FLAG_FINISH = 8;

/**
* New enigma instance.
Expand All @@ -33,104 +41,240 @@
super(NAME, true, true);
}

private static final class ParserState {
private byte indent = 0;
private byte phase = PHASE_FIND_TYPE;
private byte typeArgsIndex = 0;
/**
* {@code -1} indicates search for token start
*/
private int start = -1;
private int line = 1;
private final String[] typeArgs = new String[3];
private final Deque<String> currentClass = new ArrayDeque<>();
}

@Nonnull
@Override
public IntermediateMappings parse(@Nonnull String mappingText) {
IntermediateMappings mappings = new IntermediateMappings();
String[] lines = StringUtil.splitNewline(mappingText);
// COMMENT comment
return parse0(mappingText);
}

@Nonnull
public static IntermediateMappings parse0(@Nonnull String mappingText) {
// COMMENT comment #ignored
// CLASS BaseClass TargetClass
// FIELD baseField targetField baseDesc
// METHOD baseMethod targetMethod baseMethodDesc
// ARG baseArg targetArg
Stack<String> currentClass = new Stack<>();
for (int i = 0; i < lines.length; i++) {
String lineStr = lines[i];
String lineStrTrim = lineStr.trim();
if (lineStrTrim.isBlank())
continue;
int strIndent = lineStr.indexOf(lineStrTrim) + 1;
String[] args = lineStrTrim.split(" ");
if (args.length == 0)
continue;
String type = args[0];
try {
switch (type) {
case "CLASS":
if (lineStr.matches("\\s+.+")) {
// Check for indentation, implies the class is an inner
currentClass.add(removeNonePackage(args[1]));
} else {
// Root level class
currentClass.clear();
currentClass.add(removeNonePackage(args[1]));
// ARG baseArg targetArg #ignored
// CLASS 1 InnerClass
// FIELD innerField targetField innerDesc
IntermediateMappings mappings = new IntermediateMappings();
ParserState state = new ParserState();
for (int i = 0; i < mappingText.length(); i++) {
char c = mappingText.charAt(i);
if (c != '\n' && c != '\r' && c != '#' && c != ' ') {
if (state.phase == PHASE_IGNORE_LINE) continue; // inside # or ignored type
if (state.phase == PHASE_FIND_TYPE) {
if (c == '\t') { // read tab
state.indent++;
continue;
}
while (state.indent < state.currentClass.size()) {
state.currentClass.pop();

Check warning on line 84 in recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java#L84

Added line #L84 was not covered by tests
}
}
// If indent is lower than current class depth, pop

// start of new token
if (state.start == -1) {
state.start = i;
}
} else { // newline, #, <space>
boolean isSpace = c == ' ';
if (isSpace || state.phase != PHASE_IGNORE_LINE && state.phase < PHASE_TYPE_FLAG_FINISH) {
// finished reading a token
handleToken(mappingText, state, i);
}
if (isSpace) continue; // continue parsing next token

// newline or comment -> current line finished
writeCurrentMapping(state, mappings);

if (c == '#') {
state.phase = 0; // skip
continue;

Check warning on line 106 in recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java#L105-L106

Added lines #L105 - L106 were not covered by tests
}

state.line++;
// skip two-char newline to not count 1 new line twice
if (c == '\r' && mappingText.length() > i + 1 && mappingText.charAt(i + 1) == '\n') {
i++;

Check warning on line 112 in recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java#L112

Added line #L112 was not covered by tests
}
// reset values for next line
state.phase = PHASE_FIND_TYPE;
state.indent = 0;
state.start = -1;
state.typeArgs[0] = null;
state.typeArgs[1] = null;
state.typeArgs[2] = null;
}
}
// handle and/or write last token if applicable
if (state.phase > PHASE_FIND_TYPE) {
if (state.phase < PHASE_TYPE_FLAG_FINISH) {
handleToken(mappingText, state, mappingText.length());
}
writeCurrentMapping(state, mappings);
}
return mappings;
}

private static void writeCurrentMapping(ParserState state, IntermediateMappings mappings) {
switch (state.phase) {
case PHASE_TYPE_CLASS + PHASE_TYPE_FLAG_FINISH:
if (state.typeArgsIndex == 2) {
//noinspection DataFlowIssue
mappings.addClass(state.currentClass.peek(), state.typeArgs[1]);
}
break;
case PHASE_TYPE_FIELD + PHASE_TYPE_FLAG_FINISH:
if (state.typeArgsIndex == 3) {
String peek = state.currentClass.peek();
//noinspection DataFlowIssue
mappings.addField(peek, state.typeArgs[2], state.typeArgs[0], state.typeArgs[1]);
}
break;
case PHASE_TYPE_METHOD + PHASE_TYPE_FLAG_FINISH:
if (state.typeArgsIndex == 3) {
//noinspection DataFlowIssue
mappings.addMethod(state.currentClass.peek(), state.typeArgs[2], state.typeArgs[0], state.typeArgs[1]);
}
break;
}
}

private static void handleToken(@Nonnull String mappingText, ParserState state, int i) {
switch (state.phase) {
case PHASE_FIND_TYPE -> {
state.typeArgsIndex = 0;
String typeStr = mappingText.substring(state.start, i);
switch (typeStr) {
case "CLASS" -> state.phase = 2;
case "FIELD" -> {
if (state.currentClass.isEmpty()) {
throw new IllegalArgumentException(FAIL + "could not map field, no class context @line " + state.line + " @char " + i);

Check warning on line 166 in recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java#L166

Added line #L166 was not covered by tests
}
state.phase = PHASE_TYPE_FIELD;
}
case "METHOD" -> {
if (state.currentClass.isEmpty()) {
throw new IllegalArgumentException(FAIL + "could not map method, no class context @line " + state.line + " @char " + i);

Check warning on line 172 in recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java#L172

Added line #L172 was not covered by tests
}
state.phase = PHASE_TYPE_METHOD;
}
case "ARG", "COMMENT" -> state.phase = PHASE_IGNORE_LINE;

Check warning on line 176 in recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java#L176

Added line #L176 was not covered by tests
default -> {
LOGGER.trace("Unknown Engima mappings line type: \"{}\" @line {} @char {}", state.phase, state.line, i);
state.phase = PHASE_IGNORE_LINE;

Check warning on line 179 in recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java#L178-L179

Added lines #L178 - L179 were not covered by tests
}
}
}
case PHASE_TYPE_CLASS -> {
// <class-section> ::= <class-section-indentation> 'CLASS' <space> <class-name-a> <class-name-b>
// <formatted-access-modifier> <eol> <class-sub-sections>
// <formatted-access-modifier> ::= '' | <space> 'ACC:' <access-modifier>

// read class-name-a, class-name-b (optional)
// when finished, add FINISH_FLAG to type
String currArg = removeNonePackage(mappingText.substring(state.start, i));
switch (state.typeArgsIndex) {
case 0 -> { // class-name-a
if (!state.currentClass.isEmpty()) {
StringBuilder sb = new StringBuilder();

Check warning on line 194 in recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java#L194

Added line #L194 was not covered by tests
for (String clazz : state.currentClass) {
sb.append(clazz).append('$');
}
currArg = sb.append(currArg).toString();

Check warning on line 198 in recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java#L196-L198

Added lines #L196 - L198 were not covered by tests
}
// Not all classes need to be renamed if they have child elements that are renamed
if (args.length >= 3) {
String renamedClass = removeNonePackage(args[2]);
mappings.addClass(currentClass.peek(), renamedClass);
state.currentClass.push(currArg);
state.typeArgs[state.typeArgsIndex++] = currArg;
}
case 1 -> { // class-name-b (optional) | skip access modifier
if (currArg.isEmpty() || "-".equals(currArg) || currArg.startsWith("ACC:")) {
state.phase += PHASE_TYPE_FLAG_FINISH;
break;

Check warning on line 206 in recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java#L205-L206

Added lines #L205 - L206 were not covered by tests
}
state.typeArgs[state.typeArgsIndex++] = currArg;
state.phase += PHASE_TYPE_FLAG_FINISH;
}
}
}
case PHASE_TYPE_FIELD -> {
// <field-section> ::= <class-section-indentation> <tab> 'FIELD'<space> <field-name-a> <field-name-b>
// <formatted-access-modifier> <field-desc-a> <eol> <field-sub-sections>
// <formatted-access-modifier> ::= '' | <space> 'ACC:' <access-modifier>

// read field-name-a, field-name-b (optional), skip access modifier, read field-desc-a
// when optional, need to check the read thing is not the next one
// when finished, add FINISH_FLAG to type
String currArg = mappingText.substring(state.start, i);
switch (state.typeArgsIndex) {
case 0: // field-name-a
state.typeArgs[state.typeArgsIndex++] = currArg;
break;
case "FIELD":
// Check if no longer within inner-class scope
if (strIndent < currentClass.size())
currentClass.pop();

// Parse field
if (currentClass.empty())
throw new IllegalArgumentException(FAIL + "could not map field, no class context");

// Skip if there aren't enough arguments to pull the necessary items
if (args.length < 4)
continue;

String currentField = removeNonePackage(args[1]);
String renamedField = removeNonePackage(args[2]);
String currentFieldDesc = removeNonePackage(args[3]);
mappings.addField(currentClass.peek(), currentFieldDesc, currentField, renamedField);
case 1: // field-name-b (optional)
if (currArg.isEmpty() || "-".equals(currArg) || currArg.startsWith("ACC:")) {
state.phase += PHASE_TYPE_FLAG_FINISH;
break;

Check warning on line 229 in recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java#L228-L229

Added lines #L228 - L229 were not covered by tests
}
state.typeArgs[state.typeArgsIndex++] = currArg;
break;
case "METHOD":
// Check if no longer within inner-class scope
if (strIndent < currentClass.size())
currentClass.pop();

// Parse method
if (currentClass.empty())
throw new IllegalArgumentException(FAIL + "could not map method, no class context");

// Skip if there aren't enough arguments to pull the necessary items
if (args.length < 4)
continue;

// Skip constructors/initializers
String currentMethod = args[1];
if (currentMethod.startsWith("<"))
continue;

// Not all methods need to be renamed if they have child arg elements that are renamed
if (args.length >= 4) {
String renamedMethod = args[2];
String methodType = args[3];
mappings.addMethod(currentClass.peek(), methodType, currentMethod, renamedMethod);
case 2: // access-modifier (skip) | field-desc-a
if (currArg.isEmpty() || currArg.startsWith("ACC:")) {
break;

Check warning on line 235 in recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java#L235

Added line #L235 was not covered by tests
}
state.typeArgs[state.typeArgsIndex++] = currArg;
state.phase += PHASE_TYPE_FLAG_FINISH;
break;
case "COMMENT":
case "ARG":
// Do nothing, mapper does not support comments & arg names
}
}
case PHASE_TYPE_METHOD -> {
// <method-section> ::= <class-section-indentation> <tab> 'METHOD' <space> <method-name-a> <method-name-b>
// <formatted-access-modifier> <method-desc-a> <eol> <method-sub-sections>
// <formatted-access-modifier> ::= '' | <space> 'ACC:' <access-modifier>

// read method-name-a, method-name-b (optional), skip access modifier, read method-desc-a
// when finished, add FINISH_FLAG to type
String currArg = mappingText.substring(state.start, i);
switch (state.typeArgsIndex) {
case 0: // method-name-a
state.typeArgs[state.typeArgsIndex++] = currArg;
break;
default:
logger.trace("Unknown Engima mappings line type: \"{}\" @line {}", type, i);
case 1: // method-name-b (optional)
if (currArg.isEmpty() || "-".equals(currArg) || currArg.startsWith("ACC:")) {
state.phase += PHASE_TYPE_FLAG_FINISH;
break;

Check warning on line 257 in recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java#L256-L257

Added lines #L256 - L257 were not covered by tests
}
state.typeArgs[state.typeArgsIndex++] = currArg;
break;
case 2: // access-modifier (skip) | method-desc-a
if (currArg.isEmpty() || currArg.startsWith("ACC:")) {
break;

Check warning on line 263 in recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java#L263

Added line #L263 was not covered by tests
}
state.typeArgs[state.typeArgsIndex++] = currArg;
state.phase += PHASE_TYPE_FLAG_FINISH;
break;
}
} catch (IndexOutOfBoundsException ex) {
throw new IllegalArgumentException(FAIL + "failed parsing line " + i, ex);
}
default -> throw new IllegalStateException("Unexpected value: " + state.phase);

Check warning on line 270 in recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/services/mapping/format/EnigmaMappings.java#L270

Added line #L270 was not covered by tests
}
return mappings;
state.start = -1;
}

@Override
public String exportText(@Nonnull Mappings mappings) {
//TODO fix inner class handling
StringBuilder sb = new StringBuilder();
IntermediateMappings intermediate = mappings.exportIntermediate();
for (String oldClassName : intermediate.getClassesWithMappings()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,18 @@ void testEnigma() {
assertInheritMap(mappings);
}

@Test
void testEnigma2() {
String mappingsText = """
CLASS test/Greetings rename/Hello
\tFIELD oldField newField Ljava/lang/String;
\tMETHOD say speak ()V
""";
MappingFileFormat format = new EnigmaMappings();
IntermediateMappings mappings = assertDoesNotThrow(() -> format.parse(mappingsText));
assertInheritMap(mappings);
}

@Test
void testEnigmaWithoutEntries() {
// The mapped names are optional, so we should be able to parse a sample with no
Expand Down Expand Up @@ -168,4 +180,4 @@ private void assertInheritMap(Mappings mappings) {
assertEquals("newField", mappings.getMappedFieldName("test/Greetings", "oldField", "Ljava/lang/String;"));
assertEquals("speak", mappings.getMappedMethodName("test/Greetings", "say", "()V"));
}
}
}
Loading