All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.basinmc.plunger.mapping.mcp.parser.SRGNameMappingParser Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2018 Johannes Donath 
 * and other copyright owners as documented in the project's IP log.
 *
 * 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 org.basinmc.plunger.mapping.mcp.parser;

import edu.umd.cs.findbugs.annotations.NonNull;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.basinmc.plunger.mapping.NameMapping;

/**
 * Provides a parser for SRG based mappings.
 *
 * @author Johannes Donath
 */
public class SRGNameMappingParser {

  /**
   * @see #parse(Path, Charset)
   */
  @NonNull
  public NameMapping parse(@NonNull Path path) throws IOException {
    return this.parse(path, StandardCharsets.UTF_8);
  }

  /**
   * Parses the SRG mappings provided by an arbitrary file.
   *
   * @param path an input file.
   * @return a parsed mapping.
   * @throws IOException when accessing the file fails.
   */
  @NonNull
  public NameMapping parse(@NonNull Path path, @NonNull Charset charset) throws IOException {
    try (BufferedReader reader = Files.newBufferedReader(path, charset)) {
      return this.parse(reader);
    }
  }

  /**
   * @see #parse(InputStream, Charset)
   */
  @NonNull
  public NameMapping parse(@NonNull InputStream inputStream) throws IOException {
    return this.parse(inputStream, StandardCharsets.UTF_8);
  }

  /**
   * Parses the SRG mappings provided by an arbitrary input stream.
   *
   * @param inputStream an input stream.
   * @param charset a charset.
   * @return a parsed mapping.
   * @throws IOException when accessing the input stream fails.
   */
  @NonNull
  public NameMapping parse(@NonNull InputStream inputStream, @NonNull Charset charset)
      throws IOException {
    try (InputStreamReader reader = new InputStreamReader(inputStream, charset)) {
      return this.parse(reader);
    }
  }

  /**
   * Parses the SRG mappings provided by an arbitrary reader.
   *
   * @param reader a reader.
   * @return a parsed mapping.
   * @throws IOException when accessing the reader fails.
   */
  @NonNull
  public NameMapping parse(@NonNull Reader reader) throws IOException {
    try (BufferedReader bufferedReader = new BufferedReader(reader)) {
      return this.parse(bufferedReader);
    }
  }

  /**
   * Parses the SRG mappings provided by an arbitrary reader.
   *
   * @param reader a buffered reader.
   * @return a parsed mapping.
   */
  @NonNull
  public NameMapping parse(@NonNull BufferedReader reader) throws IOException {
    NameMappingImpl mapping = new NameMappingImpl();

    int lineNumber = 1;
    Iterator it = reader.lines().iterator();

    while (it.hasNext()) {
      String line = it.next();

      // skip any empty lines as we do not really care about them
      if (line.trim().isEmpty()) {
        continue;
      }

      String type = line.substring(0, 2);
      line = line.substring(4);

      switch (type) {
        // we are not interested in package level mappings as they're handled automatically
        case "PK":
          break;
        case "CL":
          this.parseClassMapping(mapping, lineNumber, line);
          break;
        case "FD":
          this.parseFieldMapping(mapping, lineNumber, line);
          break;
        case "MD":
          this.parseMethodMapping(mapping, lineNumber, line);
          break;
        default:
          throw new IOException(
              "Illegal instruction \"" + type + "\": Expected any of PK, CL, FD or MD");
      }

      ++lineNumber;
    }

    return mapping;
  }

  /**
   * Parses a class mapping SRG line.
   *
   * @param mapping a mapping instance.
   * @param lineNumber the current line number.
   * @param line an unparsed SRG line.
   * @throws IOException when parsing fails.
   */
  private void parseClassMapping(@NonNull NameMappingImpl mapping, int lineNumber,
      @NonNull String line) throws IOException {
    String[] elements = line.split(" ");

    if (elements.length != 2) {
      throw new IOException("Illegal class mapping on line " + lineNumber
          + ": Expected exactly two parameters - Found " + elements.length);
    }

    mapping.classNameMap.put(elements[0], elements[1]);
  }

  /**
   * Parses a field mapping SRG line.
   *
   * @param mapping a mapping instance.
   * @param lineNumber the current line number.
   * @param line an unparsed SRG line.
   * @throws IOException when parsing fails.
   */
  private void parseFieldMapping(@NonNull NameMappingImpl mapping, int lineNumber,
      @NonNull String line) throws IOException {
    String[] elements = line.split(" ");

    if (elements.length != 2) {
      throw new IOException("Illegal field mapping on line " + lineNumber
          + ": Expected exactly two parameters - Found " + elements.length);
    }

    // SRG essentially gives us two parameters here. The old fully qualified name and the new one
    // (where the field is separated from its enclosure using a single slash). Since we don't
    // support movement of fields, we'll simply split the original apart and add it to our map
    // TODO: Allow users to enable support for mapping when the enclosure has already been mapped
    int fieldSeparator = elements[0].lastIndexOf('/');

    if (fieldSeparator == -1) {
      throw new IOException(
          "Illegal field mapping on line " + lineNumber + ": No field separator in source name");
    }

    String enclosure = elements[0].substring(0, fieldSeparator);
    String originalName = elements[0].substring(fieldSeparator + 1);

    fieldSeparator = elements[1].lastIndexOf('/');

    if (fieldSeparator == -1) {
      throw new IOException(
          "Illegal field mapping on line " + lineNumber + ": No field separator in target name");
    }

    String targetName = elements[1].substring(fieldSeparator + 1);
    mapping.fieldMappings.add(new FieldMapping(enclosure, originalName, targetName));
  }

  /**
   * Parses a method mapping SRG line.
   *
   * @param mapping a mapping instance.
   * @param lineNumber the current line number.
   * @param line an unparsed SRG line.
   * @throws IOException when parsing fails.
   */
  private void parseMethodMapping(@NonNull NameMappingImpl mapping, int lineNumber,
      @NonNull String line) throws IOException {
    String[] elements = line.split(" ");

    if (elements.length != 4) {
      throw new IOException("Illegal method mapping on line " + lineNumber
          + ": Expected exactly four parameters - Found " + elements.length);
    }

    // similarly to fields, SRG will give us a full name of the class and method separated by a
    // slash (e.g. we'll need to split them up again)
    int methodSeparator = elements[0].lastIndexOf('/');

    if (methodSeparator == -1) {
      throw new IOException(
          "Illegal method mapping on line " + lineNumber + ": No method separate in source name");
    }

    String enclosure = elements[0].substring(0, methodSeparator);
    String originalName = elements[0].substring(methodSeparator + 1);

    methodSeparator = elements[2].lastIndexOf('/');

    if (methodSeparator == -1) {
      throw new IOException(
          "Illegal method mapping on line " + lineNumber + ": No method separator in target name");
    }

    String targetName = elements[2].substring(methodSeparator + 1);
    mapping.methodMappings.add(new MethodMapping(
        enclosure,
        originalName,
        targetName,
        elements[1]
    ));
  }

  /**
   * Represents a single field mapping.
   */
  private static final class FieldMapping {

    private final String className;
    private final String originalName;
    private final String targetName;

    private FieldMapping(@NonNull String className, @NonNull String originalName,
        @NonNull String targetName) {
      this.className = className;
      this.originalName = originalName;
      this.targetName = targetName;
    }

    @NonNull
    public String getTargetName() {
      return this.targetName;
    }

    public boolean matches(@NonNull String className, @NonNull String name,
        @NonNull String signature) {
      return this.className.equals(className) && this.originalName.equals(name);
    }
  }

  /**
   * Represents a single method mapping.
   */
  private static final class MethodMapping {

    private final String className;
    private final String originalName;
    private final String signature;
    private final String targetName;

    public MethodMapping(
        @NonNull String className,
        @NonNull String originalName,
        @NonNull String targetName,
        @NonNull String signature) {
      this.className = className;
      this.originalName = originalName;
      this.targetName = targetName;
      this.signature = signature;
    }

    @NonNull
    public String getTargetName() {
      return this.targetName;
    }

    public boolean matches(@NonNull String className, @NonNull String name,
        @NonNull String signature) {
      return this.className.equals(className) && this.originalName.equals(name) && this.signature
          .equals(signature);
    }
  }

  /**
   * Represents a parsed SRG mapping.
   */
  private static final class NameMappingImpl implements NameMapping {

    private final Map classNameMap = new HashMap<>();
    private final Set fieldMappings = new HashSet<>();
    private final Set methodMappings = new HashSet<>();

    /**
     * {@inheritDoc}
     */
    @NonNull
    @Override
    public Optional getClassName(@NonNull String original) {
      return Optional.ofNullable(this.classNameMap.get(original));
    }

    /**
     * {@inheritDoc}
     */
    @NonNull
    @Override
    public Optional getFieldName(@NonNull String className, @NonNull String fieldName,
        @NonNull String signature) {
      return this.fieldMappings.stream()
          .filter((m) -> m.matches(className, fieldName, signature))
          .findAny()
          .map(FieldMapping::getTargetName);
    }

    /**
     * {@inheritDoc}
     */
    @NonNull
    @Override
    public Optional getMethodName(@NonNull String className, @NonNull String methodName,
        @NonNull String signature) {
      return this.methodMappings.stream()
          .filter((m) -> m.matches(className, methodName, signature))
          .findAny()
          .map(MethodMapping::getTargetName);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy