blob: 5434832bfbe5a1797c15232c1d41d4ec81ef2c16 [file]
// 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);
}
}