package de.geolykt.starplane.remapping; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.nio.charset.StandardCharsets; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.stianloader.remapper.MappingLookup; import org.stianloader.remapper.Remapper; public class RASRemapper { @NotNull private final MappingLookup lookup; @NotNull private final StringBuilder sharedBuilder; public RASRemapper(@NotNull MappingLookup lookup, @NotNull StringBuilder sharedBuilder) { this.lookup = lookup; this.sharedBuilder = sharedBuilder; } /** * Obtain the {@link Logger} instance that should be used to log not completely fatal * warning messages. This includes instances where the access setter file is malformed, * but the line is being skipped in order to enable a minimum amount of usability. * * @return The used {@link Logger} instance */ protected Logger getLogger() { return LoggerFactory.getLogger(RASRemapper.class); } private static final boolean isBlank(@NotNull String string) { int length = string.length(); for (int i = 0; i < length; i++) { if (!Character.isWhitespace(string.codePointAt(i))) { return false; } } return true; } @NotNull private static final String codepointToString(int codepoint) { return new String(new int[] {codepoint}, 0, 1); } /** * Remap a RAS file, reading the source from a given {@link BufferedReader} and writing the remapped contents * to the given {@link BufferedWriter}. * @param reader The {@link BufferedReader} instance to read the RAS from * @param bw The {@link BufferedWriter} instance to write the remapped RAS file to * @param namespace The namespace name of the RAS file to use for logging slightly malformed line * @throws IOException Exception that is thrown if the underlying reader or writer threw an {@link IOException} * or if the read RAS file has a malformed header. */ protected void remapRAS(@NotNull BufferedReader reader, BufferedWriter bw, String namespace) throws IOException { String header = reader.readLine(); int lineNumber = 1; while (header != null && (RASRemapper.isBlank(header) || header.codePointAt(0) == '#')) { bw.write(header); bw.newLine(); header = reader.readLine(); lineNumber++; } if (header == null) { throw new IOException("Input stream exhausted before reaching RAS header for namespace " + namespace + "."); } bw.write(header); bw.newLine(); if (!header.startsWith("RAS")) { throw new IOException("Malformed ReversibleAccessSetter header of namespace " + namespace + ": Syntax error at line " + lineNumber + ": RAS header should begin with \"RAS\""); } String[] headerSplits = header.split("\\s+"); if (headerSplits.length != 3) { throw new IOException("Malformed ReversibleAccessSetter of namespace " + namespace + ": Syntax error at line " + lineNumber + ": Expected format \"RAS \"."); } String requestedFormatVersion = headerSplits[1]; String requestedDialect = headerSplits[2]; if (!(requestedFormatVersion.equals("v1") || requestedFormatVersion.equals("1") || requestedFormatVersion.equals("v1.0") || requestedFormatVersion.equals("1.1"))) { throw new IOException("ReversibleAccessSetter of namespace " + namespace + " has format version " + requestedFormatVersion + ", but this RAS implementation only supports one of ['1', 'v1', '1.1', 'v1.1']."); } if (!(requestedDialect.equals("std") || requestedDialect.equals("starrian") || requestedDialect.equals("stian"))) { throw new IOException("ReversibleAccessSetter of namespace " + namespace + " has format dialect " + requestedDialect + ", but this RAS implementation only supports one of ['std', 'starrian', 'stian']."); } lineNumber++; for (String line = reader.readLine(); line != null; line = reader.readLine(), lineNumber++) { if (RASRemapper.isBlank(line) || line.codePointAt(0) == '#') { bw.write(line); bw.newLine(); continue; } if (line.length() < 9) { // Guard against IOOBEs getLogger().error("Malformed ReversibleAccessSetter transform in line " + lineNumber + " of namespace \"" + namespace + "\": The smallest possible line length is 9 characters, but got " + line.length() + " chars instead."); continue; } int prefix = line.codePointAt(0); if (Character.isWhitespace(prefix) // According to the spec only ' ' is allowed, but the impl is a bit more lenient there. || prefix == '@' || prefix == '!') { // NOP } else if ( (Character.isWhitespace(line.codePointAt(1)) && (prefix == 'a' || prefix == 'b' || prefix == 'r')) || (Character.isWhitespace(line.codePointAt(3)) && line.startsWith("all")) || (Character.isWhitespace(line.codePointAt(5)) && line.startsWith("build")) || (Character.isWhitespace(line.codePointAt(7)) && line.startsWith("runtime"))) { // These conditions aren't allowed according to the spec, but the impl should be able to parse those cases anyways // since I expect this mistake to be done frequently. getLogger().warn("Malformed ReversibleAccessSetter transform in line {} of namespace \"{}\": Special prefixes are not optional according to the spec. Consider resolving this issue to prevent failures in other RAS implementations.", lineNumber, namespace); line = ' ' + line; prefix = ' '; } else { getLogger().error("Malformed ReversibleAccessSetter transform in line " + lineNumber + " of namespace \"" + namespace + "\": Invalid prefix " + RASRemapper.codepointToString(prefix) + "."); continue; } if (Character.isWhitespace(prefix)) { line = '0' + line.substring(1); } line = line.trim(); @NotNull String[] parts = line.split("\\s+"); if (parts.length != 4 && parts.length != 6) { getLogger().error("Malformed ReversibleAccessSetter transform in line " + lineNumber + " of namespace " + namespace + ": Expected format \"scope \" or \"scope \". (Consists of " + parts.length + " parts, but expected 4 or 6 parts)"); continue; } String scope = parts[0].substring(1); if (scope.isEmpty()) { getLogger().error("Malformed ReversibleAccessSetter transform in line " + lineNumber + " of namespace " + namespace + ": Empty scope"); continue; } if (!scope.equals("a") && !scope.equals("all") && !scope.equals("b") && !scope.equals("build") && !scope.equals("r") && !scope.equals("runtime")) { getLogger().error("Malformed ReversibleAccessSetter transform in line " + lineNumber + " of namespace " + namespace + ": Unknown scope \"" + scope + "\". Make sure you use the right dialect!"); continue; } int leftAccess = AccessUtil.parseAccess(parts[1]); int rightAccess = AccessUtil.parseAccess(parts[2]); int leftAccessType = AccessUtil.getAccessCategory(parts[1]); int rightAccessType = AccessUtil.getAccessCategory(parts[2]); if (leftAccess != 0 && rightAccess != 0) { boolean visibilityUnchanging = (leftAccess & AccessUtil.ANY_VISIBILITY_MODIFIER) == 0; if (visibilityUnchanging != ((rightAccess & AccessUtil.ANY_VISIBILITY_MODIFIER) == 0)) { throw new IOException("Malformed ReversibleAccessSetter transform in line " + lineNumber + " of namespace " + namespace + ": Incompatible accesses."); } else if (visibilityUnchanging && leftAccess != rightAccess) { throw new IOException("Malformed ReversibleAccessSetter transform in line " + lineNumber + " of namespace " + namespace + ": Incompatible accesses."); } } if (leftAccessType == AccessUtil.TARGET_MODULE || rightAccessType == AccessUtil.TARGET_MODULE) { getLogger().error("Malformed ReversibleAccessSetter transform in line " + lineNumber + " of namespace " + namespace + ": This access can only be applied on module-info.class entries, which cannot be changed by RAS as of v1.0."); continue; } String className = this.lookup.getRemappedClassName(parts[3]); if (parts.length == 4) { // Class mapping if ((leftAccessType != AccessUtil.TARGET_ANY && leftAccessType != AccessUtil.TARGET_CLASS) || (rightAccessType != AccessUtil.TARGET_ANY && rightAccessType != AccessUtil.TARGET_CLASS)) { getLogger().error("Malformed ReversibleAccessSetter transform in line " + lineNumber + " of namespace " + namespace + ": This access cannot be applied on classes."); continue; } line = RASRemapper.codepointToString(prefix) + scope + " " + parts[1] + " " + parts[2] + " " + className; } else { String memberName = parts[4]; String memberDesc = parts[5]; if (memberDesc.codePointAt(0) == '(') { // Method if ((leftAccessType != AccessUtil.TARGET_ANY && leftAccessType != AccessUtil.TARGET_METHOD) || (rightAccessType != AccessUtil.TARGET_ANY && rightAccessType != AccessUtil.TARGET_METHOD)) { getLogger().error("Malformed ReversibleAccessSetter transform in line " + lineNumber + " of namespace " + namespace + ": This access cannot be applied on methods."); continue; } memberName = this.lookup.getRemappedMethodName(className, memberName, memberDesc); memberDesc = Remapper.getRemappedMethodDescriptor(this.lookup, memberDesc, this.sharedBuilder); } else { // Field if ((leftAccessType != AccessUtil.TARGET_ANY && leftAccessType != AccessUtil.TARGET_FIELD) || (rightAccessType != AccessUtil.TARGET_ANY && rightAccessType != AccessUtil.TARGET_FIELD)) { getLogger().error("Malformed ReversibleAccessSetter transform in line " + lineNumber + " of namespace " + namespace + ": This access cannot be applied on fields."); continue; } memberName = this.lookup.getRemappedFieldName(parts[3], memberName, memberDesc); memberDesc = Remapper.getRemappedFieldDescriptor(this.lookup, memberDesc, this.sharedBuilder); } line = RASRemapper.codepointToString(prefix) + scope + " " + parts[1] + " " + parts[2] + " " + className + " " + memberName + " " + memberDesc; } bw.write(line); bw.newLine(); } } public byte[] transform(byte[] data, @NotNull String namespace) throws IOException { StringWriter writer = new StringWriter(); BufferedWriter bw = new BufferedWriter(writer); BufferedReader source = new BufferedReader(new StringReader(new String(data, StandardCharsets.UTF_8))); this.remapRAS(source, bw, namespace); bw.flush(); return writer.toString().getBytes(StandardCharsets.UTF_8); } }