| // Copyright (c) 2026, the R8 project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| package com.android.tools.r8.apimodel; |
| |
| import com.android.tools.r8.references.ClassReference; |
| import com.android.tools.r8.references.Reference; |
| import com.android.tools.r8.utils.AndroidApiLevel; |
| import com.google.common.collect.ImmutableList; |
| import java.io.BufferedReader; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.nio.charset.StandardCharsets; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.stream.Collectors; |
| |
| /** |
| * Parser for API and Jar amendments files (*.api.txt and *.jar.txt). This format is only written |
| * and read internally so it is free to change. |
| * |
| * <p>Empty lines and lines starting with {@code //} or {@code #} are ignored. |
| * |
| * <p><b>API Amendments format (*.api.txt):</b> |
| * |
| * <ul> |
| * <li>{@code class <class-descriptor> <api-level>} |
| * <li>{@code method <holder-descriptor> <method-name> <method-descriptor> <api-level>} |
| * <li>{@code field <holder-descriptor> <field-name> <api-level>} |
| * </ul> |
| * |
| * <p><b>Jar Amendments format (*.jar.txt):</b> |
| * |
| * <ul> |
| * <li>{@code class <class-descriptor> extends <super-descriptor> implements <interfaces|none>} |
| * <li>{@code interface <interface-descriptor> extends <interfaces|none>} |
| * <li>{@code method <holder-descriptor> <method-name> <method-descriptor> <static|instance>} |
| * <li>{@code field <holder-descriptor> <field-name>} |
| * </ul> |
| * |
| * <p>Multiple interfaces are comma-separated without spaces (e.g. {@code |
| * Ljava/lang/Cloneable;,Ljava/io/Serializable;}) or {@code none} if there are no interfaces. |
| */ |
| public class ApiAmendmentsParser { |
| |
| public static ApiAmendments parseApiAmendments(InputStream inputStream) throws IOException { |
| ApiAmendments amendments = new ApiAmendments(); |
| try (BufferedReader reader = |
| new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { |
| String line; |
| int lineNumber = 0; |
| while ((line = reader.readLine()) != null) { |
| lineNumber++; |
| String[] parts = splitLine(line); |
| if (parts == null) { |
| continue; |
| } |
| parseApiLine(parts[0], parts, amendments, lineNumber); |
| } |
| } |
| return amendments; |
| } |
| |
| private static void parseApiLine( |
| String type, String[] parts, ApiAmendments amendments, int lineNumber) { |
| switch (type) { |
| case "class": |
| parseClassAmendment(parts, amendments, lineNumber); |
| break; |
| case "method": |
| parseMethodAmendment(parts, amendments, lineNumber); |
| break; |
| case "field": |
| parseFieldAmendment(parts, amendments, lineNumber); |
| break; |
| default: |
| throw new RuntimeException("Unknown type " + type + " at line " + lineNumber); |
| } |
| } |
| |
| private static void parseClassAmendment( |
| String[] parts, ApiAmendments amendments, int lineNumber) { |
| if (parts.length != 3) { |
| throw new RuntimeException("Invalid class amendment at line " + lineNumber); |
| } |
| ClassReference classReference = Reference.classFromDescriptor(parts[1]); |
| AndroidApiLevel apiLevel = AndroidApiLevel.parseAndroidApiLevel(parts[2]); |
| amendments.addClass(classReference, apiLevel); |
| } |
| |
| private static void parseMethodAmendment( |
| String[] parts, ApiAmendments amendments, int lineNumber) { |
| if (parts.length != 5) { |
| throw new RuntimeException("Invalid method amendment at line " + lineNumber); |
| } |
| ClassReference holder = Reference.classFromDescriptor(parts[1]); |
| String name = parts[2]; |
| String descriptor = parts[3]; |
| AndroidApiLevel apiLevel = AndroidApiLevel.parseAndroidApiLevel(parts[4]); |
| amendments.addMethod(Reference.methodFromDescriptor(holder, name, descriptor), apiLevel); |
| } |
| |
| private static void parseFieldAmendment( |
| String[] parts, ApiAmendments amendments, int lineNumber) { |
| if (parts.length != 4) { |
| throw new RuntimeException("Invalid field amendment at line " + lineNumber); |
| } |
| ClassReference holder = Reference.classFromDescriptor(parts[1]); |
| String name = parts[2]; |
| AndroidApiLevel apiLevel = AndroidApiLevel.parseAndroidApiLevel(parts[3]); |
| amendments.addField(new FieldTypelessReference(holder, name), apiLevel); |
| } |
| |
| public static JarAmendments parseJarAmendments(InputStream inputStream) throws IOException { |
| JarAmendments amendments = new JarAmendments(); |
| try (BufferedReader reader = |
| new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { |
| String line; |
| int lineNumber = 0; |
| while ((line = reader.readLine()) != null) { |
| lineNumber++; |
| String[] parts = splitLine(line); |
| if (parts == null) { |
| continue; |
| } |
| parseJarLine(parts[0], parts, amendments, lineNumber); |
| } |
| } |
| return amendments; |
| } |
| |
| private static void parseJarLine( |
| String type, String[] parts, JarAmendments amendments, int lineNumber) { |
| switch (type) { |
| case "class": |
| parseJarClassAmendment(parts, amendments, lineNumber); |
| break; |
| case "interface": |
| parseJarInterfaceAmendment(parts, amendments, lineNumber); |
| break; |
| case "method": |
| parseJarMethodAmendment(parts, amendments, lineNumber); |
| break; |
| case "field": |
| parseJarFieldAmendment(parts, amendments, lineNumber); |
| break; |
| default: |
| throw new RuntimeException("Unknown type " + type + " at line " + lineNumber); |
| } |
| } |
| |
| private static String[] splitLine(String line) { |
| line = line.trim(); |
| if (line.isEmpty() || line.startsWith("//") || line.startsWith("#")) { |
| return null; |
| } else { |
| String[] split = line.split(" "); |
| assert split.length >= 1; |
| return split; |
| } |
| } |
| |
| private static void parseJarClassAmendment( |
| String[] parts, JarAmendments amendments, int lineNumber) { |
| if (parts.length != 6) { |
| throw new RuntimeException("Invalid jar class amendment at line " + lineNumber); |
| } |
| ClassReference classReference = Reference.classFromDescriptor(parts[1]); |
| if (!parts[2].equals("extends")) { |
| throw new RuntimeException("Expected 'extends' as third word at line " + lineNumber); |
| } |
| ClassReference superClass = Reference.classFromDescriptor(parts[3]); |
| if (!parts[4].equals("implements")) { |
| throw new RuntimeException("Expected 'implements' as fifth word at line " + lineNumber); |
| } |
| List<ClassReference> interfaces = parseInterfaces(parts[5]); |
| amendments.addClass(classReference, superClass, interfaces); |
| } |
| |
| private static void parseJarInterfaceAmendment( |
| String[] parts, JarAmendments amendments, int lineNumber) { |
| if (parts.length != 4) { |
| throw new RuntimeException("Invalid jar interface amendment at line " + lineNumber); |
| } |
| ClassReference interfaceReference = Reference.classFromDescriptor(parts[1]); |
| if (!parts[2].equals("extends")) { |
| throw new RuntimeException("Expected 'extends' as third word at line " + lineNumber); |
| } |
| List<ClassReference> interfaces = parseInterfaces(parts[3]); |
| amendments.addInterface(interfaceReference, interfaces); |
| } |
| |
| private static List<ClassReference> parseInterfaces(String interfacesStr) { |
| if (interfacesStr.equals("none")) { |
| return ImmutableList.of(); |
| } |
| return Arrays.stream(interfacesStr.split(",")) |
| .map(Reference::classFromDescriptor) |
| .collect(Collectors.toList()); |
| } |
| |
| private static void parseJarMethodAmendment( |
| String[] parts, JarAmendments amendments, int lineNumber) { |
| if (parts.length != 5) { |
| throw new RuntimeException("Invalid jar method amendment at line " + lineNumber); |
| } |
| ClassReference holder = Reference.classFromDescriptor(parts[1]); |
| String name = parts[2]; |
| String descriptor = parts[3]; |
| String relation = parts[4]; |
| boolean isStatic; |
| if (relation.equals("static")) { |
| isStatic = true; |
| } else if (relation.equals("instance")) { |
| isStatic = false; |
| } else { |
| throw new RuntimeException( |
| "Expected 'static' or 'instance' as fifth word at line " + lineNumber); |
| } |
| amendments.addMethod(holder, name, descriptor, isStatic); |
| } |
| |
| private static void parseJarFieldAmendment( |
| String[] parts, JarAmendments amendments, int lineNumber) { |
| if (parts.length != 3) { |
| throw new RuntimeException("Invalid jar field amendment at line " + lineNumber); |
| } |
| ClassReference holder = Reference.classFromDescriptor(parts[1]); |
| String name = parts[2]; |
| amendments.addField(holder, name); |
| } |
| } |